Ticket #11216: jcs-tms-v09.patch
| File jcs-tms-v09.patch, 99.2 KB (added by , 11 years ago) |
|---|
-
src/org/apache/commons
-
build.xml
Property changes on: src/org/apache/commons ___________________________________________________________________ Added: svn:externals ## -0,0 +1,2 ## +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 loggingIndex: build.xmlIndex: build.xml
220 220 destdir="build" target="1.7" source="1.7" debug="on" includeantruntime="false" createMissingPackageInfoClass="false" encoding="iso-8859-1"> 221 221 <!-- get rid of "internal proprietary API" warning --> 222 222 <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"/> 223 231 </javac> 224 232 <!-- JMapViewer/JOSM --> 225 233 <javac srcdir="${src.dir}" excludes="com/**,oauth/**,org/apache/commons/**,org/glassfish/**,org/openstreetmap/gui/jmapviewer/Demo.java" -
src/org/openstreetmap/josm/Main.java
67 67 import org.openstreetmap.josm.data.ProjectionBounds; 68 68 import org.openstreetmap.josm.data.UndoRedoHandler; 69 69 import org.openstreetmap.josm.data.ViewportData; 70 import org.openstreetmap.josm.data.cache.JCSCacheManager; 70 71 import org.openstreetmap.josm.data.coor.CoordinateFormat; 71 72 import org.openstreetmap.josm.data.coor.LatLon; 72 73 import org.openstreetmap.josm.data.osm.DataSet; … … 963 964 } 964 965 965 966 geometry = WindowGeometry.mainWindow("gui.geometry", 966 (args.containsKey(Option.GEOMETRY) ? args.get(Option.GEOMETRY).iterator().next() : null),967 !args.containsKey(Option.NO_MAXIMIZE) && Main.pref.getBoolean("gui.maximized", false));967 (args.containsKey(Option.GEOMETRY) ? args.get(Option.GEOMETRY).iterator().next() : null), 968 !args.containsKey(Option.NO_MAXIMIZE) && Main.pref.getBoolean("gui.maximized", false)); 968 969 } 969 970 970 971 protected static void postConstructorProcessCmdLine(Map<Option, Collection<String>> args) { … … 1088 1089 * @since 3378 1089 1090 */ 1090 1091 public static boolean exitJosm(boolean exit, int exitCode) { 1092 JCSCacheManager.shutdown(); 1091 1093 if (Main.saveUnsavedModifications()) { 1092 1094 geometry.remember("gui.geometry"); 1093 1095 if (map != null) { -
src/org/openstreetmap/josm/data/cache/JCSCacheManager.java
1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.cache; 3 4 import java.io.File; 5 import java.io.IOException; 6 import java.text.MessageFormat; 7 import java.util.Properties; 8 import java.util.logging.Handler; 9 import java.util.logging.Level; 10 import java.util.logging.LogRecord; 11 import java.util.logging.Logger; 12 13 import org.apache.commons.jcs.access.CacheAccess; 14 import org.apache.commons.jcs.auxiliary.AuxiliaryCache; 15 import org.apache.commons.jcs.auxiliary.disk.indexed.IndexedDiskCache; 16 import org.apache.commons.jcs.auxiliary.disk.indexed.IndexedDiskCacheAttributes; 17 import org.apache.commons.jcs.auxiliary.disk.indexed.IndexedDiskCacheManager; 18 import org.apache.commons.jcs.engine.CompositeCacheAttributes; 19 import org.apache.commons.jcs.engine.behavior.ICompositeCacheAttributes.DiskUsagePattern; 20 import org.apache.commons.jcs.engine.control.CompositeCache; 21 import org.apache.commons.jcs.engine.control.CompositeCacheManager; 22 import org.apache.commons.jcs.utils.serialization.StandardSerializer; 23 import org.openstreetmap.gui.jmapviewer.FeatureAdapter; 24 import org.openstreetmap.josm.Main; 25 import org.openstreetmap.josm.data.preferences.IntegerProperty; 26 27 28 /** 29 * @author Wiktor Niesiobędzki 30 * 31 * Wrapper class for JCS Cache. Sets some sane environment and returns instances of cache objects. 32 * Static configuration for now assumes some small LRU cache in memory and larger LRU cache on disk 33 * 34 */ 35 public class JCSCacheManager { 36 private static final Logger log = FeatureAdapter.getLogger(JCSCacheManager.class.getCanonicalName()); 37 38 private static volatile CompositeCacheManager cacheManager = null; 39 private static long maxObjectTTL = Long.MAX_VALUE; 40 private final static String PREFERENCE_PREFIX = "jcs.cache"; 41 42 /** 43 * default objects to be held in memory by JCS caches (per region) 44 */ 45 public static final IntegerProperty DEFAULT_MAX_OBJECTS_IN_MEMORY = new IntegerProperty(PREFERENCE_PREFIX + ".max_objects_in_memory", 1000); 46 47 private static void initialize() throws IOException { 48 File cacheDir = new File(Main.pref.getCacheDirectory(), "jcs"); 49 50 if ((!cacheDir.exists() && !cacheDir.mkdirs())) 51 throw new IOException("Cannot access cache directory"); 52 53 // raising logging level gives ~500x performance gain 54 // http://westsworld.dk/blog/2008/01/jcs-and-performance/ 55 Logger jcsLog = Logger.getLogger("org.apache.commons.jcs"); 56 jcsLog.setLevel(Level.INFO); 57 jcsLog.setUseParentHandlers(false); 58 //Logger.getLogger("org.apache.common").setUseParentHandlers(false); 59 // we need a separate handler from Main's, as we downgrade LEVEL.INFO to DEBUG level 60 jcsLog.addHandler(new Handler() { 61 @Override 62 public void publish(LogRecord record) { 63 String msg = MessageFormat.format(record.getMessage(), record.getParameters()); 64 if (record.getLevel().intValue() >= Level.SEVERE.intValue()) { 65 Main.error(msg); 66 } else if (record.getLevel().intValue() >= Level.WARNING.intValue()) { 67 Main.warn(msg); 68 // downgrade INFO level to debug, as JCS is too verbose at INFO level 69 } else if (record.getLevel().intValue() >= Level.INFO.intValue()) { 70 Main.debug(msg); 71 } else { 72 Main.trace(msg); 73 } 74 } 75 76 @Override 77 public void flush() { 78 } 79 80 @Override 81 public void close() throws SecurityException { 82 } 83 }); 84 85 86 CompositeCacheManager cm = CompositeCacheManager.getUnconfiguredInstance(); 87 // this could be moved to external file 88 Properties props = new Properties(); 89 // these are default common to all cache regions 90 // use of auxiliary cache and sizing of the caches is done with giving proper geCache(...) params 91 props.setProperty("jcs.default.cacheattributes", org.apache.commons.jcs.engine.CompositeCacheAttributes.class.getCanonicalName()); 92 props.setProperty("jcs.default.cacheattributes.MaxObjects", DEFAULT_MAX_OBJECTS_IN_MEMORY.get().toString()); 93 props.setProperty("jcs.default.cacheattributes.UseMemoryShrinker", "true"); 94 props.setProperty("jcs.default.cacheattributes.DiskUsagePatternName", "UPDATE"); // store elements on disk on put 95 props.setProperty("jcs.default.elementattributes", CacheEntryAttributes.class.getCanonicalName()); 96 props.setProperty("jcs.default.elementattributes.IsEternal", "false"); 97 props.setProperty("jcs.default.elementattributes.MaxLife", Long.toString(maxObjectTTL)); 98 props.setProperty("jcs.default.elementattributes.IdleTime", Long.toString(maxObjectTTL)); 99 props.setProperty("jcs.default.elementattributes.IsSpool", "true"); 100 cm.configure(props); 101 cacheManager = cm; 102 103 } 104 105 /** 106 * Returns configured cache object for named cache region 107 * @param cacheName region name 108 * @return cache access object 109 * @throws IOException if directory is not found 110 */ 111 public static <K,V> CacheAccess<K, V> getCache(String cacheName) throws IOException { 112 return getCache(cacheName, DEFAULT_MAX_OBJECTS_IN_MEMORY.get().intValue(), 0, null); 113 } 114 115 /** 116 * Returns configured cache object with defined limits of memory cache and disk cache 117 * @param cacheName region name 118 * @param maxMemoryObjects number of objects to keep in memory 119 * @param maxDiskObjects number of objects to keep on disk (if cachePath provided) 120 * @param cachePath path to disk cache. if null, no disk cache will be created 121 * @return cache access object 122 * @throws IOException if directory is not found 123 */ 124 public static <K,V> CacheAccess<K, V> getCache(String cacheName, int maxMemoryObjects, int maxDiskObjects, String cachePath) throws IOException { 125 if (cacheManager != null) 126 return getCacheInner(cacheName, maxMemoryObjects, maxDiskObjects, cachePath); 127 128 synchronized (JCSCacheManager.class) { 129 if (cacheManager == null) 130 initialize(); 131 return getCacheInner(cacheName, maxMemoryObjects, maxDiskObjects, cachePath); 132 } 133 } 134 135 136 @SuppressWarnings("unchecked") 137 private static <K,V> CacheAccess<K, V> getCacheInner(String cacheName, int maxMemoryObjects, int maxDiskObjects, String cachePath) { 138 CompositeCache<K, V> cc = cacheManager.getCache(cacheName, getCacheAttributes(maxMemoryObjects)); 139 140 if (cachePath != null) { 141 IndexedDiskCacheAttributes diskAttributes = getDiskCacheAttributes(maxDiskObjects, cachePath); 142 diskAttributes.setCacheName(cacheName); 143 IndexedDiskCache<K, V> diskCache = IndexedDiskCacheManager.getInstance(null, null, new StandardSerializer()).getCache(diskAttributes); 144 cc.setAuxCaches(new AuxiliaryCache[]{diskCache}); 145 } 146 return new CacheAccess<K, V>(cc); 147 } 148 149 /** 150 * Close all files to ensure, that all indexes and data are properly written 151 */ 152 public static void shutdown() { 153 // use volatile semantics to get consistent object 154 CompositeCacheManager localCacheManager = cacheManager; 155 if (localCacheManager != null) { 156 localCacheManager.shutDown(); 157 } 158 } 159 160 private static IndexedDiskCacheAttributes getDiskCacheAttributes(int maxDiskObjects, String cachePath) { 161 IndexedDiskCacheAttributes ret = new IndexedDiskCacheAttributes(); 162 ret.setMaxKeySize(maxDiskObjects); 163 if (cachePath != null) { 164 File path = new File(cachePath); 165 if (!path.exists() && !path.mkdirs()) { 166 log.log(Level.WARNING, "Failed to create cache path: {0}", cachePath); 167 } else { 168 ret.setDiskPath(path); 169 } 170 } 171 return ret; 172 } 173 174 private static CompositeCacheAttributes getCacheAttributes(int maxMemoryElements) { 175 CompositeCacheAttributes ret = new CompositeCacheAttributes(); 176 ret.setMaxObjects(maxMemoryElements); 177 ret.setDiskUsagePattern(DiskUsagePattern.UPDATE); 178 return ret; 179 } 180 } -
src/org/openstreetmap/josm/data/cache/BufferedImageCacheEntry.java
1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.cache; 3 4 import java.awt.image.BufferedImage; 5 import java.io.ByteArrayInputStream; 6 import java.io.IOException; 7 8 import javax.imageio.ImageIO; 9 10 /** 11 * Cache Entry that has methods to get the BufferedImage, that will be cached along in memory 12 * but will be not serialized when saved to the disk (to avoid duplication of data) 13 * @author Wiktor Niesiobędzki 14 * 15 */ 16 public class BufferedImageCacheEntry extends CacheEntry { 17 private static final long serialVersionUID = 1L; //version 18 // transient to avoid serialization, volatile to avoid synchronization of whole getImage() method 19 private transient volatile BufferedImage img = null; 20 21 /** 22 * 23 * @param content byte array containing image 24 */ 25 public BufferedImageCacheEntry(byte[] content) { 26 super(content); 27 } 28 29 /** 30 * Returns BufferedImage from for the content. Subsequent calls will return the same instance, 31 * to reduce overhead of ImageIO 32 * 33 * @return BufferedImage of cache entry content 34 * @throws IOException 35 */ 36 public BufferedImage getImage() throws IOException { 37 if (img != null) 38 return img; 39 synchronized(this) { 40 if (img != null) 41 return img; 42 img = ImageIO.read(new ByteArrayInputStream(getContent())); 43 } 44 return img; 45 } 46 } -
src/org/openstreetmap/josm/data/cache/CacheEntry.java
1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.cache; 3 4 import java.io.Serializable; 5 6 /** 7 * @author Wiktor Niesiobędzki 8 * 9 * Class that will hold JCS cache entries 10 * 11 */ 12 public 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/ICachedLoaderListener.java
1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.cache; 3 4 public interface ICachedLoaderListener { 5 /** 6 * Will be called when K object was successfully downloaded 7 * 8 * @param data 9 * @param success 10 */ 11 public void loadingFinished(CacheEntry data, boolean success); 12 13 } -
src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.cache; 3 4 import java.io.ByteArrayOutputStream; 5 import java.io.FileNotFoundException; 6 import java.io.IOException; 7 import java.io.InputStream; 8 import java.net.HttpURLConnection; 9 import java.net.MalformedURLException; 10 import java.net.URLConnection; 11 import java.util.HashSet; 12 import java.util.Map; 13 import java.util.Random; 14 import java.util.Set; 15 import java.util.concurrent.ConcurrentHashMap; 16 import java.util.concurrent.ConcurrentMap; 17 import java.util.concurrent.Executor; 18 import java.util.concurrent.LinkedBlockingDeque; 19 import java.util.concurrent.RejectedExecutionException; 20 import java.util.concurrent.ThreadPoolExecutor; 21 import java.util.concurrent.TimeUnit; 22 import java.util.logging.Level; 23 import java.util.logging.Logger; 24 25 import org.apache.commons.jcs.access.behavior.ICacheAccess; 26 import org.apache.commons.jcs.engine.behavior.ICacheElement; 27 import org.openstreetmap.gui.jmapviewer.FeatureAdapter; 28 import org.openstreetmap.josm.data.preferences.IntegerProperty; 29 30 /** 31 * @author Wiktor Niesiobędzki 32 * 33 * @param <K> cache entry key type 34 * 35 * Generic loader for HTTP based tiles. Uses custom attribute, to check, if entry has expired 36 * according to HTTP headers sent with tile. If so, it tries to verify using Etags 37 * or If-Modified-Since / Last-Modified. 38 * 39 * If the tile is not valid, it will try to download it from remote service and put it 40 * to cache. If remote server will fail it will try to use stale entry. 41 * 42 * This class will keep only one Job running for specified tile. All others will just finish, but 43 * listeners will be gathered and notified, once download job will be finished 44 */ 45 public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements ICachedLoaderJob<K>, Runnable { 46 private static final Logger log = FeatureAdapter.getLogger(JCSCachedTileLoaderJob.class.getCanonicalName()); 47 protected static final long DEFAULT_EXPIRE_TIME = 1000L * 60 * 60 * 24 * 7; // 7 days 48 // Limit for the max-age value send by the server. 49 protected static final long EXPIRE_TIME_SERVER_LIMIT = 1000L * 60 * 60 * 24 * 28; // 4 weeks 50 // Absolute expire time limit. Cached tiles that are older will not be used, 51 // even if the refresh from the server fails. 52 protected static final long ABSOLUTE_EXPIRE_TIME_LIMIT = Long.MAX_VALUE; // unlimited 53 54 /** 55 * maximum download threads that will be started 56 */ 57 public static IntegerProperty THREAD_LIMIT = new IntegerProperty("cache.jcs.max_threads", 10); 58 private static Executor DOWNLOAD_JOB_DISPATCHER = new ThreadPoolExecutor( 59 2, // we have a small queue, so threads will be quickly started (threads are started only, when queue is full) 60 THREAD_LIMIT.get().intValue(), // do not this number of threads 61 30, // keepalive for thread 62 TimeUnit.SECONDS, 63 // make queue of LIFO type - so recently requested tiles will be loaded first (assuming that these are which user is waiting to see) 64 new LinkedBlockingDeque<Runnable>(5) { 65 /* keep the queue size fairly small, we do not want to 66 download a lot of tiles, that user is not seeing anyway */ 67 @Override 68 public boolean offer(Runnable t) { 69 return super.offerFirst(t); 70 } 71 72 @Override 73 public Runnable remove() { 74 return super.removeFirst(); 75 } 76 } 77 ); 78 private static ConcurrentMap<String,Set<ICachedLoaderListener>> inProgress = new ConcurrentHashMap<>(); 79 private static ConcurrentMap<String, Boolean> useHead = new ConcurrentHashMap<>(); 80 81 private long now; // when the job started 82 83 private ICacheAccess<K, V> cache; 84 private ICacheElement<K, V> cacheElement; 85 protected V cacheData = null; 86 protected CacheEntryAttributes attributes = null; 87 88 // HTTP connection parameters 89 private int connectTimeout; 90 private int readTimeout; 91 private Map<String, String> headers; 92 93 /** 94 * @param cache cache instance that we will work on 95 * @param headers 96 * @param readTimeout 97 * @param connectTimeout 98 */ 99 public JCSCachedTileLoaderJob(ICacheAccess<K,V> cache, 100 int connectTimeout, int readTimeout, 101 Map<String, String> headers) { 102 103 this.cache = cache; 104 this.now = System.currentTimeMillis(); 105 this.connectTimeout = connectTimeout; 106 this.readTimeout = readTimeout; 107 this.headers = headers; 108 } 109 110 private void ensureCacheElement() { 111 if (cacheElement == null && getCacheKey() != null) { 112 cacheElement = cache.getCacheElement(getCacheKey()); 113 if (cacheElement != null) { 114 attributes = (CacheEntryAttributes) cacheElement.getElementAttributes(); 115 cacheData = cacheElement.getVal(); 116 } 117 } 118 } 119 120 public V get() { 121 ensureCacheElement(); 122 return cacheData; 123 } 124 125 @Override 126 public void submit(ICachedLoaderListener listener) { 127 boolean first = false; 128 String url = getUrl().toString(); 129 if (url == null) { 130 log.log(Level.WARNING, "No url returned for: {0}, skipping", getCacheKey()); 131 return; 132 } 133 synchronized (inProgress) { 134 Set<ICachedLoaderListener> newListeners = inProgress.get(url); 135 if (newListeners == null) { 136 newListeners = new HashSet<>(); 137 inProgress.put(url, newListeners); 138 first = true; 139 } 140 newListeners.add(listener); 141 } 142 143 if (first) { 144 ensureCacheElement(); 145 if (cacheElement != null && isCacheElementValid() && (isObjectLoadable())) { 146 // we got something in cache, and it's valid, so lets return it 147 log.log(Level.FINE, "JCS - Returning object from cache: {0}", getCacheKey()); 148 finishLoading(true); 149 return; 150 } 151 // object not in cache, so submit work to separate thread 152 try { 153 // use getter method, so subclasses may override executors, to get separate thread pool 154 getDownloadExecutor().execute(JCSCachedTileLoaderJob.this); 155 } catch (RejectedExecutionException e) { 156 // queue was full, try again later 157 log.log(Level.FINE, "JCS - rejected job for: {0}", getCacheKey()); 158 finishLoading(false); 159 } 160 } 161 } 162 163 /** 164 * 165 * @return checks if object from cache has sufficient data to be returned 166 */ 167 protected boolean isObjectLoadable() { 168 byte[] content = cacheData.getContent(); 169 return content != null && content.length > 0; 170 } 171 172 /** 173 * 174 * @return cache object as empty, regardless of what remote resource has returned (ex. based on headers) 175 */ 176 protected boolean cacheAsEmpty() { 177 return false; 178 } 179 180 /** 181 * @return key under which discovered server settings will be kept 182 */ 183 protected String getServerKey() { 184 return getUrl().getHost(); 185 } 186 187 /** 188 * this needs to be non-static, so it can be overridden by subclasses 189 */ 190 protected Executor getDownloadExecutor() { 191 return DOWNLOAD_JOB_DISPATCHER; 192 } 193 194 195 public void run() { 196 final Thread currentThread = Thread.currentThread(); 197 final String oldName = currentThread.getName(); 198 currentThread.setName("JCS Downloading: " + getUrl()); 199 try { 200 // try to load object from remote resource 201 if (loadObject()) { 202 finishLoading(true); 203 } else { 204 // if loading failed - check if we can return stale entry 205 if (isObjectLoadable()) { 206 // try to get stale entry in cache 207 finishLoading(true); 208 log.log(Level.FINE, "JCS - found stale object in cache: {0}", getUrl()); 209 } else { 210 // failed completely 211 finishLoading(false); 212 } 213 } 214 } finally { 215 currentThread.setName(oldName); 216 } 217 } 218 219 220 private void finishLoading(boolean success) { 221 Set<ICachedLoaderListener> listeners = null; 222 synchronized (inProgress) { 223 listeners = inProgress.remove(getUrl().toString()); 224 } 225 if (listeners == null) { 226 log.log(Level.WARNING, "Listener not found for URL: {0}. Listener not notified!", getUrl()); 227 return; 228 } 229 try { 230 for (ICachedLoaderListener l: listeners) { 231 l.loadingFinished(cacheData, success); 232 } 233 } catch (Exception e) { 234 log.log(Level.WARNING, "JCS - Error while loading object from cache: {0}; {1}", new Object[]{e.getMessage(), getUrl()}); 235 log.log(Level.FINE, "Stacktrace", e); 236 for (ICachedLoaderListener l: listeners) { 237 l.loadingFinished(cacheData, false); 238 } 239 240 } 241 242 } 243 244 private boolean isCacheElementValid() { 245 long expires = attributes.getExpirationTime(); 246 247 // check by expire date set by server 248 if (expires != 0L) { 249 // put a limit to the expire time (some servers send a value 250 // that is too large) 251 expires = Math.min(expires, attributes.getCreateTime() + EXPIRE_TIME_SERVER_LIMIT); 252 if (now > expires) { 253 log.log(Level.FINE, "JCS - Object {0} has expired -> valid to {1}, now is: {2}", new Object[]{getUrl(), Long.toString(expires), Long.toString(now)}); 254 return false; 255 } 256 } else { 257 // check by file modification date 258 if (now - attributes.getLastModification() > DEFAULT_EXPIRE_TIME) { 259 log.log(Level.FINE, "JCS - Object has expired, maximum file age reached {0}", getUrl()); 260 return false; 261 } 262 } 263 return true; 264 } 265 266 private boolean loadObject() { 267 try { 268 // if we have object in cache, and host doesn't support If-Modified-Since nor If-None-Match 269 // then just use HEAD request and check returned values 270 if (isObjectLoadable() && 271 Boolean.TRUE.equals(useHead.get(getServerKey())) && 272 isCacheValidUsingHead()) { 273 log.log(Level.FINE, "JCS - cache entry verified using HEAD request: {0}", getUrl()); 274 return true; 275 } 276 URLConnection urlConn = getURLConnection(); 277 278 if (isObjectLoadable() && 279 (now - attributes.getLastModification()) <= ABSOLUTE_EXPIRE_TIME_LIMIT) { 280 urlConn.setIfModifiedSince(attributes.getLastModification()); 281 } 282 if (isObjectLoadable() && attributes.getEtag() != null) { 283 urlConn.addRequestProperty("If-None-Match", attributes.getEtag()); 284 } 285 if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 304) { 286 // If isModifiedSince or If-None-Match has been set 287 // and the server answers with a HTTP 304 = "Not Modified" 288 log.log(Level.FINE, "JCS - IfModifiedSince/Etag test: local version is up to date: {0}", getUrl()); 289 return true; 290 } else if (isObjectLoadable()) { 291 // we have an object in cache, but we haven't received 304 resposne code 292 // check if we should use HEAD request to verify 293 if((attributes.getEtag() != null && attributes.getEtag().equals(urlConn.getRequestProperty("ETag"))) || 294 attributes.getLastModification() == urlConn.getLastModified()) { 295 // we sent ETag or If-Modified-Since, but didn't get 304 response code 296 // for further requests - use HEAD 297 String serverKey = getServerKey(); 298 log.log(Level.INFO, "JCS - Host: {0} found not to return 304 codes for If-Modifed-Since or If-None-Match headers", serverKey); 299 useHead.put(serverKey, Boolean.TRUE); 300 } 301 } 302 303 attributes = parseHeaders(urlConn); 304 305 for (int i = 0; i < 5; ++i) { 306 if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 503) { 307 Thread.sleep(5000+(new Random()).nextInt(5000)); 308 continue; 309 } 310 byte[] raw = read(urlConn); 311 312 if (!cacheAsEmpty() && raw != null && raw.length > 0) { 313 cacheData = createCacheEntry(raw); 314 cache.put(getCacheKey(), cacheData, attributes); 315 log.log(Level.FINE, "JCS - downloaded key: {0}, length: {1}, url: {2}", 316 new Object[] {getCacheKey(), raw.length, getUrl()}); 317 return true; 318 } else { 319 cacheData = createCacheEntry(new byte[]{}); 320 cache.put(getCacheKey(), cacheData, attributes); 321 log.log(Level.FINE, "JCS - Caching empty object {0}", getUrl()); 322 return true; 323 } 324 } 325 } catch (FileNotFoundException e) { 326 log.log(Level.FINE, "JCS - Caching empty object as server returned 404 for: {0}", getUrl()); 327 cache.put(getCacheKey(), createCacheEntry(new byte[]{}), attributes); 328 handleNotFound(); 329 return true; 330 } catch (Exception e) { 331 log.log(Level.WARNING, "JCS - Exception during download " + getUrl(), e); 332 } 333 log.log(Level.WARNING, "JCS - Silent failure during download: {0}", getUrl()); 334 return false; 335 336 } 337 338 protected abstract void handleNotFound(); 339 340 protected abstract V createCacheEntry(byte[] content); 341 342 private CacheEntryAttributes parseHeaders(URLConnection urlConn) { 343 CacheEntryAttributes ret = new CacheEntryAttributes(); 344 ret.setNoTileAtZoom("no-tile".equals(urlConn.getHeaderField("X-VE-Tile-Info"))); 345 346 Long lng = urlConn.getExpiration(); 347 if (lng.equals(0L)) { 348 try { 349 String str = urlConn.getHeaderField("Cache-Control"); 350 if (str != null) { 351 for (String token: str.split(",")) { 352 if (token.startsWith("max-age=")) { 353 lng = Long.parseLong(token.substring(8)) * 1000 + 354 System.currentTimeMillis(); 355 } 356 } 357 } 358 } catch (NumberFormatException e) {} //ignore malformed Cache-Control headers 359 } 360 361 ret.setExpirationTime(lng); 362 ret.setLastModification(now); 363 ret.setEtag(urlConn.getHeaderField("ETag")); 364 return ret; 365 } 366 367 private HttpURLConnection getURLConnection() throws IOException, MalformedURLException { 368 HttpURLConnection urlConn = (HttpURLConnection) getUrl().openConnection(); 369 urlConn.setRequestProperty("Accept", "text/html, image/png, image/jpeg, image/gif, */*"); 370 urlConn.setReadTimeout(readTimeout); // 30 seconds read timeout 371 urlConn.setConnectTimeout(connectTimeout); 372 for(Map.Entry<String, String> e: headers.entrySet()) { 373 urlConn.setRequestProperty(e.getKey(), e.getValue()); 374 } 375 return urlConn; 376 } 377 378 private boolean isCacheValidUsingHead() throws IOException { 379 HttpURLConnection urlConn = (HttpURLConnection) getUrl().openConnection(); 380 urlConn.setRequestMethod("HEAD"); 381 long lastModified = urlConn.getLastModified(); 382 return ( 383 (attributes.getEtag() != null && attributes.getEtag().equals(urlConn.getRequestProperty("ETag"))) || 384 (lastModified != 0 && lastModified <= attributes.getLastModification()) 385 ); 386 } 387 388 private static byte[] read(URLConnection urlConn) throws IOException { 389 InputStream input = urlConn.getInputStream(); 390 try { 391 ByteArrayOutputStream bout = new ByteArrayOutputStream(input.available()); 392 byte[] buffer = new byte[2048]; 393 boolean finished = false; 394 do { 395 int read = input.read(buffer); 396 if (read >= 0) { 397 bout.write(buffer, 0, read); 398 } else { 399 finished = true; 400 } 401 } while (!finished); 402 if (bout.size() == 0) 403 return null; 404 return bout.toByteArray(); 405 } finally { 406 input.close(); 407 } 408 } 409 } -
src/org/openstreetmap/josm/data/cache/CacheEntryAttributes.java
1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.cache; 3 4 import java.util.HashMap; 5 import java.util.Map; 6 7 import org.apache.commons.jcs.engine.ElementAttributes; 8 9 /** 10 * Class that contains attirubtes for JCS cache entries. Parameters are used to properly handle HTTP caching 11 * 12 * @author Wiktor Niesiobędzki 13 * 14 */ 15 public class CacheEntryAttributes extends ElementAttributes { 16 private static final long serialVersionUID = 1L; //version 17 private Map<String, String> attrs = new HashMap<String, String>(); 18 private final static String NO_TILE_AT_ZOOM = "noTileAtZoom"; 19 private final static String ETAG = "Etag"; 20 private final static String LAST_MODIFICATION = "lastModification"; 21 private final static String EXPIRATION_TIME = "expirationTime"; 22 23 public CacheEntryAttributes() { 24 super(); 25 attrs.put(NO_TILE_AT_ZOOM, "false"); 26 attrs.put(ETAG, null); 27 attrs.put(LAST_MODIFICATION, "0"); 28 attrs.put(EXPIRATION_TIME, "0"); 29 } 30 31 public boolean isNoTileAtZoom() { 32 return Boolean.toString(true).equals(attrs.get(NO_TILE_AT_ZOOM)); 33 } 34 public void setNoTileAtZoom(boolean noTileAtZoom) { 35 attrs.put(NO_TILE_AT_ZOOM, Boolean.toString(noTileAtZoom)); 36 } 37 public String getEtag() { 38 return attrs.get(ETAG); 39 } 40 public void setEtag(String etag) { 41 attrs.put(ETAG, etag); 42 } 43 44 private long getLongAttr(String key) { 45 try { 46 return Long.parseLong(attrs.get(key)); 47 } catch (NumberFormatException e) { 48 attrs.put(key, "0"); 49 return 0; 50 } 51 } 52 53 public long getLastModification() { 54 return getLongAttr(LAST_MODIFICATION); 55 } 56 public void setLastModification(long lastModification) { 57 attrs.put(LAST_MODIFICATION, Long.toString(lastModification)); 58 } 59 public long getExpirationTime() { 60 return getLongAttr(EXPIRATION_TIME); 61 } 62 public void setExpirationTime(long expirationTime) { 63 attrs.put(EXPIRATION_TIME, Long.toString(expirationTime)); 64 } 65 66 } -
src/org/openstreetmap/josm/data/cache/ICachedLoaderJob.java
1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.cache; 3 4 import java.net.URL; 5 6 7 /** 8 * 9 * @author Wiktor Niesiobędzki 10 * 11 * @param <K> cache key type 12 */ 13 public interface ICachedLoaderJob<K> { 14 /** 15 * returns cache entry key 16 * 17 * @param tile 18 * @return cache key for tile 19 */ 20 public K getCacheKey(); 21 22 /** 23 * method to get download URL for Job 24 * @return URL that should be fetched 25 * 26 */ 27 public URL getUrl(); 28 /** 29 * implements the main algorithm for fetching 30 */ 31 public void run(); 32 33 /** 34 * fetches object from cache, or returns null when object is not found 35 * 36 * @return filled tile with data or null when no cache entry found 37 */ 38 public CacheEntry get(); 39 40 /** 41 * Submit job for background fetch, and listener will be 42 * fed with value object 43 * 44 * @param listener 45 */ 46 public void submit(ICachedLoaderListener listener); 47 } -
src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoader.java
1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery; 3 4 import java.io.IOException; 5 import java.util.Map; 6 7 import org.apache.commons.jcs.access.behavior.ICacheAccess; 8 import org.openstreetmap.gui.jmapviewer.Tile; 9 import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader; 10 import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; 11 import org.openstreetmap.gui.jmapviewer.interfaces.TileJob; 12 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 13 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 14 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 15 import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry; 16 import org.openstreetmap.josm.data.cache.JCSCacheManager; 17 import org.openstreetmap.josm.data.preferences.IntegerProperty; 18 19 /** 20 * @author Wiktor Niesiobędzki 21 * 22 * Wrapper class that bridges between JCS cache and Tile Loaders 23 * 24 */ 25 public class TMSCachedTileLoader implements TileLoader, CachedTileLoader, TileCache { 26 27 private ICacheAccess<String, BufferedImageCacheEntry> cache; 28 private int connectTimeout; 29 private int readTimeout; 30 private Map<String, String> headers; 31 private TileLoaderListener listener; 32 public static final String PREFERENCE_PREFIX = "imagery.tms.cache."; 33 // average tile size is about 20kb 34 public static IntegerProperty MAX_OBJECTS_ON_DISK = new IntegerProperty(PREFERENCE_PREFIX + "max_objects_disk", 25000); // 25000 is around 500MB under this assumptions 35 36 37 /** 38 * Constructor 39 * @param listener called when tile loading has finished 40 * @param name of the cache 41 * @param connectTimeout to remote resource 42 * @param readTimeout to remote resource 43 * @param headers to be sent along with request 44 * @param cacheDir where cache file shall reside 45 * @throws IOException when cache initialization fails 46 */ 47 public TMSCachedTileLoader(TileLoaderListener listener, String name, int connectTimeout, int readTimeout, Map<String, String> headers, String cacheDir) throws IOException { 48 this.cache = JCSCacheManager.getCache(name, 49 1000, // use JCS memory cache instead of MemoryTileCache 50 MAX_OBJECTS_ON_DISK.get(), 51 cacheDir); 52 this.connectTimeout = connectTimeout; 53 this.readTimeout = readTimeout; 54 this.headers = headers; 55 this.listener = listener; 56 } 57 58 @Override 59 public TileJob createTileLoaderJob(Tile tile) { 60 return new TMSCachedTileLoaderJob(listener, tile, cache, connectTimeout, readTimeout, headers); 61 } 62 63 @Override 64 public void clearCache() { 65 this.cache.clear(); 66 } 67 68 @Override 69 public Tile getTile(TileSource source, int x, int y, int z) { 70 return createTileLoaderJob(new Tile(source,x, y, z)).getTile(); 71 } 72 73 @Override 74 public void addTile(Tile tile) { 75 createTileLoaderJob(tile).submit(); 76 } 77 78 @Override 79 public int getTileCount() { 80 return 0; 81 } 82 83 @Override 84 public void clear() { 85 cache.clear(); 86 } 87 88 public String getStats() { 89 return cache.getStats(); 90 } 91 } -
src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery; 3 4 import java.io.ByteArrayInputStream; 5 import java.io.IOException; 6 import java.net.URL; 7 import java.util.Map; 8 import java.util.concurrent.Executor; 9 import java.util.concurrent.LinkedBlockingDeque; 10 import java.util.concurrent.ThreadPoolExecutor; 11 import java.util.concurrent.TimeUnit; 12 import java.util.logging.Level; 13 import java.util.logging.Logger; 14 15 import org.apache.commons.jcs.access.behavior.ICacheAccess; 16 import org.openstreetmap.gui.jmapviewer.FeatureAdapter; 17 import org.openstreetmap.gui.jmapviewer.Tile; 18 import org.openstreetmap.gui.jmapviewer.interfaces.TileJob; 19 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 20 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 21 import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource; 22 import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry; 23 import org.openstreetmap.josm.data.cache.CacheEntry; 24 import org.openstreetmap.josm.data.cache.ICachedLoaderListener; 25 import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob; 26 import org.openstreetmap.josm.data.preferences.IntegerProperty; 27 28 /** 29 * @author Wiktor Niesiobędzki 30 * 31 * Class bridging TMS requests to JCS cache requests 32 * 33 */ 34 public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, BufferedImageCacheEntry> implements TileJob, ICachedLoaderListener { 35 private static final Logger log = FeatureAdapter.getLogger(TMSCachedTileLoaderJob.class.getCanonicalName()); 36 private Tile tile; 37 private TileLoaderListener listener; 38 private volatile URL url; 39 40 /** 41 * overrides the THREAD_LIMIT in superclass, as we want to have separate limit and pool for TMS 42 */ 43 public static IntegerProperty THREAD_LIMIT = new IntegerProperty("imagery.tms.tmsloader.maxjobs", 25); 44 /** 45 * separate from JCS thread pool for TMS loader, so we can have different thread pools for default JCS 46 * and for TMS imagery 47 */ 48 private static ThreadPoolExecutor DOWNLOAD_JOB_DISPATCHER = new ThreadPoolExecutor( 49 THREAD_LIMIT.get().intValue(), // keep the thread number constant 50 THREAD_LIMIT.get().intValue(), // do not this number of threads 51 30, // keepalive for thread 52 TimeUnit.SECONDS, 53 // make queue of LIFO type - so recently requested tiles will be loaded first (assuming that these are which user is waiting to see) 54 new LinkedBlockingDeque<Runnable>(5) { 55 /* keep the queue size fairly small, we do not want to 56 download a lot of tiles, that user is not seeing anyway */ 57 @Override 58 public boolean offer(Runnable t) { 59 return super.offerFirst(t); 60 } 61 62 @Override 63 public Runnable remove() { 64 return super.removeFirst(); 65 } 66 } 67 ); 68 69 /** 70 * Constructor for creating a job, to get a specific tile from cache 71 * @param listener 72 * @param tile to be fetched from cache 73 * @param cache object 74 * @param connectTimeout when connecting to remote resource 75 * @param readTimeout when connecting to remote resource 76 * @param headers to be sent together with request 77 */ 78 public TMSCachedTileLoaderJob(TileLoaderListener listener, Tile tile, ICacheAccess<String, BufferedImageCacheEntry> cache, int connectTimeout, int readTimeout, 79 Map<String, String> headers) { 80 super(cache, connectTimeout, readTimeout, headers); 81 this.tile = tile; 82 this.listener = listener; 83 } 84 85 @Override 86 public Tile getTile() { 87 return getCachedTile(); 88 } 89 90 @Override 91 public String getCacheKey() { 92 if (tile != null) 93 return tile.getKey(); 94 return null; 95 } 96 97 /* 98 * this doesn't needs to be synchronized, as it's not that costly to keep only one execution 99 * in parallel, but URL creation and Tile.getUrl() are costly and are not needed when fetching 100 * data from cache 101 * 102 * We need to have static url value for TileLoaderJob, as for some TileSources we might get different 103 * URL's each call we made (servers switching), and URL's are used below as a key for duplicate detection 104 * 105 */ 106 @Override 107 public URL getUrl() { 108 if (url == null) { 109 try { 110 url = new URL(tile.getUrl()); 111 } catch (IOException e) { 112 log.log(Level.WARNING, "JCS TMS Cache - error creating URL for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()}); 113 log.log(Level.INFO, "Exception: ", e); 114 } 115 } 116 return url; 117 } 118 119 @Override 120 public boolean isObjectLoadable() { 121 if (cacheData != null) { 122 byte[] content = cacheData.getContent(); 123 return (content != null && content.length > 0) || cacheAsEmpty(); 124 } 125 return false; 126 } 127 128 @Override 129 protected boolean cacheAsEmpty() { 130 if (attributes != null && attributes.isNoTileAtZoom()) { 131 // do not remove file - keep the information, that there is no tile, for further requests 132 // the code above will check, if this information is still valid 133 log.log(Level.FINE, "JCS TMS - Tile valid, but no file, as no tiles at this level {0}", tile); 134 tile.setError("No tile at this zoom level"); 135 tile.putValue("tile-info", "no-tile"); 136 return true; 137 } 138 return false; 139 } 140 141 @Override 142 protected Executor getDownloadExecutor() { 143 return DOWNLOAD_JOB_DISPATCHER; 144 } 145 146 public void submit() { 147 tile.initLoading(); 148 super.submit(this); 149 } 150 151 @Override 152 public void loadingFinished(CacheEntry object, boolean success) { 153 try { 154 loadTile(object); 155 if (listener != null) { 156 listener.tileLoadingFinished(tile, success); 157 } 158 } catch (IOException e) { 159 log.log(Level.WARNING, "JCS TMS - error loading object for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()}); 160 tile.setError(e.getMessage()); 161 tile.setLoaded(false); 162 if (listener != null) { 163 listener.tileLoadingFinished(tile, false); 164 } 165 } 166 } 167 168 /** 169 * Method for getting the tile from cache only, without trying to reach remote resource 170 * @return tile or null, if nothing (useful) was found in cache 171 */ 172 public Tile getCachedTile() { 173 BufferedImageCacheEntry data = super.get(); 174 if (isObjectLoadable()) { 175 try { 176 loadTile(data); 177 return tile; 178 } catch (IOException e) { 179 log.log(Level.WARNING, "JCS TMS - error loading object for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()}); 180 return null; 181 } 182 183 } else { 184 return null; 185 } 186 } 187 188 private void loadTile(CacheEntry object) throws IOException { 189 tile.finishLoading(); 190 if (object != null && object.getContent() != null && object.getContent().length > 0) { 191 if (object instanceof BufferedImageCacheEntry) { 192 tile.setImage(((BufferedImageCacheEntry)object).getImage()); 193 } else { 194 tile.loadImage(new ByteArrayInputStream(object.getContent())); 195 } 196 } 197 } 198 199 @Override 200 protected void handleNotFound() { 201 tile.setError("No tile at this zoom level"); 202 tile.putValue("tile-info", "no-tile"); 203 } 204 205 @Override 206 protected String getServerKey() { 207 TileSource ts = tile.getSource(); 208 if (ts instanceof AbstractTMSTileSource) { 209 return ((AbstractTMSTileSource) ts).getBaseUrl(); 210 } 211 return super.getServerKey(); 212 } 213 214 @Override 215 protected BufferedImageCacheEntry createCacheEntry(byte[] content) { 216 return new BufferedImageCacheEntry(content); 217 } 218 } -
src/org/openstreetmap/josm/gui/preferences/imagery/TMSSettingsPanel.java
11 11 import javax.swing.JSpinner; 12 12 import javax.swing.SpinnerNumberModel; 13 13 14 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 15 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoaderJob; 14 16 import org.openstreetmap.josm.gui.layer.TMSLayer; 15 import org.openstreetmap.josm.tools.GBC;16 17 import org.openstreetmap.josm.gui.widgets.JosmTextField; 18 import org.openstreetmap.josm.tools.GBC; 17 19 18 20 /** 19 21 * {@code JPanel} giving access to TMS settings. … … 28 30 private final JSpinner maxZoomLvl; 29 31 private final JCheckBox addToSlippyMapChosser = new JCheckBox(); 30 32 private final JosmTextField tilecacheDir = new JosmTextField(); 33 private final JSpinner maxElementsOnDisk; 34 private final JSpinner maxConcurrentDownloads; 35 31 36 32 37 /** 33 38 * Constructs a new {@code TMSSettingsPanel}. … … 36 41 super(new GridBagLayout()); 37 42 minZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.DEFAULT_MIN_ZOOM, TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1)); 38 43 maxZoomLvl = new JSpinner(new SpinnerNumberModel(TMSLayer.DEFAULT_MAX_ZOOM, TMSLayer.MIN_ZOOM, TMSLayer.MAX_ZOOM, 1)); 44 maxElementsOnDisk = new JSpinner(new SpinnerNumberModel(TMSCachedTileLoader.MAX_OBJECTS_ON_DISK.get().intValue(), 0, Integer.MAX_VALUE, 1)); 45 maxConcurrentDownloads = new JSpinner(new SpinnerNumberModel(TMSCachedTileLoaderJob.THREAD_LIMIT.get().intValue(), 0, Integer.MAX_VALUE, 1)); 39 46 40 47 add(new JLabel(tr("Auto zoom by default: ")), GBC.std()); 41 48 add(GBC.glue(5, 0), GBC.std()); … … 60 67 add(new JLabel(tr("Tile cache directory: ")), GBC.std()); 61 68 add(GBC.glue(5, 0), GBC.std()); 62 69 add(tilecacheDir, GBC.eol().fill(GBC.HORIZONTAL)); 70 71 add(new JLabel(tr("Maximum concurrent downloads: ")), GBC.std()); 72 add(GBC.glue(5, 0), GBC.std()); 73 add(maxConcurrentDownloads, GBC.eol()); 74 75 add(new JLabel(tr("Maximum elements in disk cache: ")), GBC.std()); 76 add(GBC.glue(5, 0), GBC.std()); 77 add(this.maxElementsOnDisk, GBC.eol()); 78 63 79 } 64 80 65 81 /** 66 82 * Loads the TMS settings. 67 83 */ … … 72 88 this.maxZoomLvl.setValue(TMSLayer.getMaxZoomLvl(null)); 73 89 this.minZoomLvl.setValue(TMSLayer.getMinZoomLvl(null)); 74 90 this.tilecacheDir.setText(TMSLayer.PROP_TILECACHE_DIR.get()); 91 this.maxElementsOnDisk.setValue(TMSCachedTileLoader.MAX_OBJECTS_ON_DISK.get()); 92 this.maxConcurrentDownloads.setValue(TMSCachedTileLoaderJob.THREAD_LIMIT.get()); 75 93 } 76 94 77 95 /** 78 96 * Saves the TMS settings. 79 97 * @return true when restart is required 80 98 */ 81 99 public boolean saveSettings() { 82 100 boolean restartRequired = false; 83 101 84 102 if (TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.get() != this.addToSlippyMapChosser.isSelected()) { 85 103 restartRequired = true; 86 104 } … … 89 107 TMSLayer.PROP_DEFAULT_AUTOLOAD.put(this.autoloadTiles.isSelected()); 90 108 TMSLayer.setMaxZoomLvl((Integer)this.maxZoomLvl.getValue()); 91 109 TMSLayer.setMinZoomLvl((Integer)this.minZoomLvl.getValue()); 92 TMSLayer.PROP_TILECACHE_DIR.put(this.tilecacheDir.getText()); 93 110 111 TMSCachedTileLoader.MAX_OBJECTS_ON_DISK.put((Integer) this.maxElementsOnDisk.getValue()); 112 113 if (!TMSCachedTileLoaderJob.THREAD_LIMIT.get().equals(this.maxConcurrentDownloads.getValue())) { 114 restartRequired = true; 115 TMSCachedTileLoaderJob.THREAD_LIMIT.put((Integer) this.maxConcurrentDownloads.getValue()); 116 } 117 118 if (!TMSLayer.PROP_TILECACHE_DIR.get().equals(this.tilecacheDir.getText())) { 119 restartRequired = true; 120 TMSLayer.PROP_TILECACHE_DIR.put(this.tilecacheDir.getText()); 121 } 122 94 123 return restartRequired; 95 124 } 96 125 } -
src/org/openstreetmap/josm/gui/layer/TMSLayer.java
21 21 import java.net.URL; 22 22 import java.util.ArrayList; 23 23 import java.util.Collections; 24 import java.util.Hash Set;24 import java.util.HashMap; 25 25 import java.util.LinkedList; 26 26 import java.util.List; 27 27 import java.util.Map; 28 import java.util.Map.Entry;29 28 import java.util.Scanner; 30 import java.util.Set;31 29 import java.util.concurrent.Callable; 32 30 import java.util.regex.Matcher; 33 31 import java.util.regex.Pattern; … … 41 39 42 40 import org.openstreetmap.gui.jmapviewer.AttributionSupport; 43 41 import org.openstreetmap.gui.jmapviewer.Coordinate; 44 import org.openstreetmap.gui.jmapviewer.JobDispatcher;45 42 import org.openstreetmap.gui.jmapviewer.MemoryTileCache; 46 import org.openstreetmap.gui.jmapviewer.OsmFileCacheTileLoader;47 43 import org.openstreetmap.gui.jmapviewer.OsmTileLoader; 48 import org.openstreetmap.gui.jmapviewer.TMSFileCacheTileLoader;49 44 import org.openstreetmap.gui.jmapviewer.Tile; 50 45 import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader; 51 import org.openstreetmap.gui.jmapviewer.interfaces.TileClearController; 46 import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; 47 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 52 48 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 53 49 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 54 50 import org.openstreetmap.gui.jmapviewer.tilesources.BingAerialTileSource; … … 63 59 import org.openstreetmap.josm.data.coor.LatLon; 64 60 import org.openstreetmap.josm.data.imagery.ImageryInfo; 65 61 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType; 62 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 66 63 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 67 64 import org.openstreetmap.josm.data.preferences.BooleanProperty; 68 65 import org.openstreetmap.josm.data.preferences.IntegerProperty; … … 75 72 import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 76 73 import org.openstreetmap.josm.gui.dialogs.LayerListPopup; 77 74 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 78 import org.openstreetmap.josm.gui.progress.ProgressMonitor.CancelListener;79 75 import org.openstreetmap.josm.io.CacheCustomContent; 80 76 import org.openstreetmap.josm.io.OsmTransferException; 81 77 import org.openstreetmap.josm.io.UTFInputStreamReader; … … 84 80 import org.xml.sax.InputSource; 85 81 import org.xml.sax.SAXException; 86 82 83 84 87 85 /** 88 86 * Class that displays a slippy map layer. 89 87 * … … 108 106 public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", DEFAULT_MAX_ZOOM); 109 107 //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false); 110 108 public static final BooleanProperty PROP_ADD_TO_SLIPPYMAP_CHOOSER = new BooleanProperty(PREFERENCE_PREFIX + ".add_to_slippymap_chooser", true); 111 public static final IntegerProperty PROP_TMS_JOBS = new IntegerProperty("tmsloader.maxjobs", 25);112 109 public static final StringProperty PROP_TILECACHE_DIR; 113 110 114 111 static { … … 122 119 } 123 120 124 121 public interface TileLoaderFactory { 125 OsmTileLoader makeTileLoader(TileLoaderListener listener); 122 TileLoader makeTileLoader(TileLoaderListener listener); 123 TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers); 126 124 } 127 125 128 protected MemoryTileCache tileCache;126 protected TileCache tileCache; 129 127 protected TileSource tileSource; 130 protected OsmTileLoader tileLoader; 128 protected TileLoader tileLoader; 129 131 130 132 131 public static TileLoaderFactory loaderFactory = new TileLoaderFactory() { 133 132 @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 } 133 public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) { 134 Map<String, String> headers = new HashMap<>(); 135 headers.put("User-Agent", Version.getInstance().getFullAgentString()); 136 headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*"); 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 PROP_TILECACHE_DIR.get()); 146 } catch (IOException e) { 147 Main.warn(e); 145 148 } 146 149 return null; 147 150 } 151 152 @Override 153 public TileLoader makeTileLoader(TileLoaderListener listener) { 154 return makeTileLoader(listener, null); 155 } 148 156 }; 149 157 150 158 /** 151 159 * Plugins that wish to set custom tile loader should call this method 152 160 */ 161 153 162 public static void setCustomTileLoaderFactory(TileLoaderFactory loaderFactory) { 154 163 TMSLayer.loaderFactory = loaderFactory; 155 164 } 156 165 157 private Set<Tile> tileRequestsOutstanding = new HashSet<>();158 159 166 @Override 160 167 public synchronized void tileLoadingFinished(Tile tile, boolean success) { 161 168 if (tile.hasError()) { … … 165 172 if (sharpenLevel != 0 && success) { 166 173 tile.setImage(sharpenImage(tile.getImage())); 167 174 } 168 tile.setLoaded( true);175 tile.setLoaded(success); 169 176 needRedraw = true; 170 177 if (Main.map != null) { 171 178 Main.map.repaint(100); 172 179 } 173 tileRequestsOutstanding.remove(tile);174 180 if (Main.isDebugEnabled()) { 175 181 Main.debug("tileLoadingFinished() tile: " + tile + " success: " + success); 176 182 } 177 183 } 178 184 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 @Override190 public void initClearDir(File dir) {191 }192 193 @Override194 public void initClearFiles(File[] files) {195 monitor.setTicksCount(files.length);196 monitor.setTicks(0);197 }198 199 @Override200 public boolean cancel() {201 return cancel;202 }203 204 @Override205 public void fileDeleted(File file) {206 monitor.setTicks(monitor.getTicks()+1);207 }208 209 @Override210 public void clearFinished() {211 monitor.finishTask();212 }213 214 @Override215 public void operationCanceled() {216 cancel = true;217 }218 }219 220 185 /** 221 186 * Clears the tile cache. 222 187 * … … 225 190 * method. 226 191 * 227 192 * @param monitor 228 * @see MemoryTileCache#clear()229 193 * @see OsmFileCacheTileLoader#clearCache(org.openstreetmap.gui.jmapviewer.interfaces.TileSource, org.openstreetmap.gui.jmapviewer.interfaces.TileClearController) 230 194 */ 231 195 void clearTileCache(ProgressMonitor monitor) { 232 196 tileCache.clear(); 233 197 if (tileLoader instanceof CachedTileLoader) { 234 ((CachedTileLoader)tileLoader).clearCache( tileSource, new TmsTileClearController(monitor));198 ((CachedTileLoader)tileLoader).clearCache(); 235 199 } 236 200 } 237 201 … … 415 379 416 380 currentZoomLevel = getBestZoom(); 417 381 418 tileCache = new MemoryTileCache(); 419 420 tileLoader = loaderFactory.makeTileLoader(this); 421 if (tileLoader == null) { 422 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; 382 Map<String, String> headers = null; 426 383 if (tileSource instanceof TemplatedTMSTileSource) { 427 for(Entry<String, String> e : ((TemplatedTMSTileSource)tileSource).getHeaders().entrySet()) { 428 tileLoader.headers.put(e.getKey(), e.getValue()); 429 } 384 headers = (((TemplatedTMSTileSource)tileSource).getHeaders()); 430 385 } 431 tileLoader.headers.put("User-Agent", Version.getInstance().getFullAgentString());432 }433 386 434 @Override 435 public void setOffset(double dx, double dy) { 436 super.setOffset(dx, dy); 437 needRedraw = true; 387 // FIXME: tileCache = new MemoryTileCache(); 388 tileLoader = loaderFactory.makeTileLoader(this, headers); 389 if (tileLoader instanceof TMSCachedTileLoader) { 390 tileCache = (TileCache) tileLoader; 391 } else { 392 tileCache = new MemoryTileCache(); 393 } 394 if (tileLoader == null) 395 tileLoader = new OsmTileLoader(this); 438 396 } 439 397 440 398 /** … … 471 429 return intResult; 472 430 } 473 431 474 /**475 * Function to set the maximum number of workers for tile loading to the value defined476 * in preferences.477 */478 public static void setMaxWorkers() {479 JobDispatcher.setMaxWorkers(PROP_TMS_JOBS.get());480 JobDispatcher.getInstance().setLIFO(true);481 }482 483 432 @SuppressWarnings("serial") 484 433 public TMSLayer(ImageryInfo info) { 485 434 super(info); 486 435 487 setMaxWorkers();488 436 if(!isProjectionSupported(Main.getProjection())) { 489 437 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);438 tr("TMS layers do not support the projection {0}.\n{1}\n" 439 + "Change the projection or remove the layer.", 440 Main.getProjection().toCode(), nameSupportedProjections()), 441 tr("Warning"), 442 JOptionPane.WARNING_MESSAGE); 495 443 } 496 444 497 445 setBackgroundLayer(true); … … 629 577 new PleaseWaitRunnable(tr("Flush Tile Cache")) { 630 578 @Override 631 579 protected void realRun() throws SAXException, IOException, 632 OsmTransferException {580 OsmTransferException { 633 581 clearTileCache(getProgressMonitor()); 634 582 } 635 583 … … 684 632 Main.debug("zoomChanged(): " + currentZoomLevel); 685 633 } 686 634 needRedraw = true; 687 JobDispatcher.getInstance().cancelOutstandingJobs();688 tileRequestsOutstanding.clear();689 635 } 690 636 691 637 int getMaxZoomLvl() { … … 770 716 * are temporary only and intentionally not inserted 771 717 * into the tileCache. 772 718 */ 773 synchronizedTile tempCornerTile(Tile t) {719 Tile tempCornerTile(Tile t) { 774 720 int x = t.getXtile() + 1; 775 721 int y = t.getYtile() + 1; 776 722 int zoom = t.getZoom(); … … 780 726 return new Tile(tileSource, x, y, zoom); 781 727 } 782 728 783 synchronizedTile getOrCreateTile(int x, int y, int zoom) {729 Tile getOrCreateTile(int x, int y, int zoom) { 784 730 Tile tile = getTile(x, y, zoom); 785 731 if (tile == null) { 786 732 tile = new Tile(tileSource, x, y, zoom); … … 794 740 * This can and will return null for tiles that are not 795 741 * already in the cache. 796 742 */ 797 synchronizedTile getTile(int x, int y, int zoom) {743 Tile getTile(int x, int y, int zoom) { 798 744 int max = (1 << zoom); 799 745 if (x < 0 || x >= max || y < 0 || y >= max) 800 746 return null; 801 747 return tileCache.getTile(tileSource, x, y, zoom); 802 748 } 803 749 804 synchronizedboolean loadTile(Tile tile, boolean force) {750 boolean loadTile(Tile tile, boolean force) { 805 751 if (tile == null) 806 752 return false; 807 if (!force && (tile. hasError() || tile.isLoaded()))753 if (!force && (tile.isLoaded() || tile.hasError())) 808 754 return false; 809 755 if (tile.isLoading()) 810 756 return false; 811 if (tileRequestsOutstanding.contains(tile)) 812 return false; 813 tileRequestsOutstanding.add(tile); 814 JobDispatcher.getInstance().addJob(tileLoader.createTileLoaderJob(tile)); 757 tileLoader.createTileLoaderJob(tile).submit(); 815 758 return true; 816 759 } 817 760 … … 1268 1211 public TileSet getTileSet(int zoom) { 1269 1212 if (zoom < minZoom) 1270 1213 return nullTileSet; 1271 TileSet ts = tileSets[zoom-minZoom]; 1272 if (ts == null) { 1273 ts = new TileSet(topLeft, botRight, zoom); 1274 tileSets[zoom-minZoom] = ts; 1214 synchronized (tileSets) { 1215 TileSet ts = tileSets[zoom-minZoom]; 1216 if (ts == null) { 1217 ts = new TileSet(topLeft, botRight, zoom); 1218 tileSets[zoom-minZoom] = ts; 1219 } 1220 return ts; 1275 1221 } 1276 return ts;1277 1222 } 1223 1278 1224 public TileSetInfo getTileSetInfo(int zoom) { 1279 1225 if (zoom < minZoom) 1280 1226 return new TileSetInfo(); 1281 TileSetInfo tsi = tileSetInfos[zoom-minZoom]; 1282 if (tsi == null) { 1283 tsi = TMSLayer.getTileSetInfo(getTileSet(zoom)); 1284 tileSetInfos[zoom-minZoom] = tsi; 1227 synchronized (tileSetInfos) { 1228 TileSetInfo tsi = tileSetInfos[zoom-minZoom]; 1229 if (tsi == null) { 1230 tsi = TMSLayer.getTileSetInfo(getTileSet(zoom)); 1231 tileSetInfos[zoom-minZoom] = tsi; 1232 } 1233 return tsi; 1285 1234 } 1286 return tsi;1287 1235 } 1288 1236 } 1289 1237 1290 1238 @Override 1291 1239 public void paint(Graphics2D g, MapView mv, Bounds bounds) { 1292 //long start = System.currentTimeMillis();1293 1240 EastNorth topLeft = mv.getEastNorth(0, 0); 1294 1241 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight()); 1295 1242 … … 1435 1382 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155); 1436 1383 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170); 1437 1384 myDrawString(g, tr("Best zoom: {0}", Math.log(getScaleFactor(1))/Math.log(2)/2+1), 50, 185); 1385 if(tileLoader instanceof TMSCachedTileLoader) { 1386 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader)tileLoader; 1387 int offset = 185; 1388 for(String part: cachedTileLoader.getStats().split("\n")) { 1389 myDrawString(g, tr("Cache stats: {0}", part), 50, offset+=15); 1390 } 1391 1392 } 1438 1393 } 1439 1394 } 1440 1395 -
src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java
11 11 import java.util.ArrayList; 12 12 import java.util.Arrays; 13 13 import java.util.Collections; 14 import java.util.HashMap; 14 15 import java.util.HashSet; 15 16 import java.util.List; 17 import java.util.Map; 16 18 import java.util.Set; 17 19 import java.util.concurrent.CopyOnWriteArrayList; 18 20 … … 25 27 import org.openstreetmap.gui.jmapviewer.MemoryTileCache; 26 28 import org.openstreetmap.gui.jmapviewer.OsmTileLoader; 27 29 import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker; 30 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 28 31 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 29 32 import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOpenAerialTileSource; 30 33 import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOsmTileSource; … … 113 116 private static final StringProperty PROP_MAPSTYLE = new StringProperty("slippy_map_chooser.mapstyle", "Mapnik"); 114 117 public static final String RESIZE_PROP = SlippyMapBBoxChooser.class.getName() + ".resize"; 115 118 116 private OsmTileLoader cachedLoader;119 private TileLoader cachedLoader; 117 120 private OsmTileLoader uncachedLoader; 118 121 119 122 private final SizeButton iSizeButton; … … 131 134 debug = Main.isDebugEnabled(); 132 135 SpringLayout springLayout = new SpringLayout(); 133 136 setLayout(springLayout); 134 TMSLayer.setMaxWorkers(); 135 cachedLoader = TMSLayer.loaderFactory.makeTileLoader(this); 137 138 Map<String, String> headers = new HashMap<>(); 139 headers.put("User-Agent", Version.getInstance().getFullAgentString()); 140 141 cachedLoader = TMSLayer.loaderFactory.makeTileLoader(this, headers); 136 142 137 143 uncachedLoader = new OsmTileLoader(this); 138 uncachedLoader.headers.put ("User-Agent", Version.getInstance().getFullAgentString());144 uncachedLoader.headers.putAll(headers); 139 145 setZoomContolsVisible(Main.pref.getBoolean("slippy_map_chooser.zoomcontrols",false)); 140 146 setMapMarkerVisible(false); 141 147 setMinimumSize(new Dimension(350, 350 / 2)); -
src/org/openstreetmap/gui/jmapviewer/TMSFileCacheTileLoader.java
1 // License: GPL. For details, see Readme.txt file.2 package org.openstreetmap.gui.jmapviewer;3 4 import java.io.File;5 import java.io.IOException;6 import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;7 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;8 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;9 10 /**11 * Reworked version of the OsmFileCacheTileLoader.12 *13 * When class OsmFileCacheTileLoader is no longer needed, it can be integrated14 * here and removed.15 */16 public class TMSFileCacheTileLoader extends OsmFileCacheTileLoader {17 18 public TMSFileCacheTileLoader(TileLoaderListener map, File cacheDir) throws IOException {19 super(map, cacheDir);20 }21 22 @Override23 public TileJob createTileLoaderJob(final Tile tile) {24 return new TMSFileLoadJob(tile);25 }26 27 protected class TMSFileLoadJob extends FileLoadJob {28 29 public TMSFileLoadJob(Tile tile) {30 super(tile);31 }32 33 @Override34 protected File getTileFile() {35 return getDataFile(tile.getSource().getTileType());36 }37 38 @Override39 protected File getTagsFile() {40 return getDataFile(TAGS_FILE_EXT);41 }42 43 protected File getDataFile(String ext) {44 int nDigits = (int) Math.ceil(Math.log10(1 << tile.getZoom()));45 String x = String.format("%0" + nDigits + "d", tile.getXtile());46 String y = String.format("%0" + nDigits + "d", tile.getYtile());47 File path = new File(tileCacheDir, "z" + tile.getZoom());48 for (int i=0; i<nDigits; i++) {49 String component = "x" + x.substring(i, i+1) + "y" + y.substring(i, i+1);50 if (i == nDigits -1 ) {51 component += "." + ext;52 }53 path = new File(path, component);54 }55 return path;56 }57 }58 59 @Override60 protected File getSourceCacheDir(TileSource source) {61 File dir = sourceCacheDirMap.get(source);62 if (dir == null) {63 String id = source.getId();64 if (id != null) {65 dir = new File(cacheDirBase, id);66 } else {67 dir = new File(cacheDirBase, source.getName().replaceAll("[\\\\/:*?\"<>|]", "_"));68 }69 if (!dir.exists()) {70 dir.mkdirs();71 }72 }73 return dir;74 }75 76 } -
src/org/openstreetmap/gui/jmapviewer/OsmFileCacheTileLoader.java
1 // License: GPL. For details, see Readme.txt file.2 package org.openstreetmap.gui.jmapviewer;3 4 import java.io.BufferedReader;5 import java.io.ByteArrayInputStream;6 import java.io.ByteArrayOutputStream;7 import java.io.File;8 import java.io.FileInputStream;9 import java.io.FileNotFoundException;10 import java.io.FileOutputStream;11 import java.io.IOException;12 import java.io.InputStream;13 import java.io.InputStreamReader;14 import java.io.OutputStreamWriter;15 import java.io.PrintWriter;16 import java.net.HttpURLConnection;17 import java.net.URL;18 import java.net.URLConnection;19 import java.nio.charset.Charset;20 import java.util.HashMap;21 import java.util.Map;22 import java.util.Map.Entry;23 import java.util.Random;24 import java.util.logging.Level;25 import java.util.logging.Logger;26 27 import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;28 import org.openstreetmap.gui.jmapviewer.interfaces.TileClearController;29 import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;30 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;31 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;32 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;33 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource.TileUpdate;34 35 /**36 * A {@link TileLoader} implementation that loads tiles from OSM via HTTP and37 * saves all loaded files in a directory located in the temporary directory.38 * If a tile is present in this file cache it will not be loaded from OSM again.39 *40 * @author Jan Peter Stotz41 * @author Stefan Zeller42 */43 public class OsmFileCacheTileLoader extends OsmTileLoader implements CachedTileLoader {44 45 private static final Logger log = FeatureAdapter.getLogger(OsmFileCacheTileLoader.class.getName());46 47 protected static final String TAGS_FILE_EXT = "tags";48 49 private static final Charset TAGS_CHARSET = Charset.forName("UTF-8");50 51 // Default expire time (i.e. maximum age of cached tile before refresh).52 // Used when the server does not send an expires or max-age value in the http header.53 protected static final long DEFAULT_EXPIRE_TIME = 1000L * 60 * 60 * 24 * 7; // 7 days54 // Limit for the max-age value send by the server.55 protected static final long EXPIRE_TIME_SERVER_LIMIT = 1000L * 60 * 60 * 24 * 28; // 4 weeks56 // Absolute expire time limit. Cached tiles that are older will not be used,57 // even if the refresh from the server fails.58 protected static final long ABSOLUTE_EXPIRE_TIME_LIMIT = Long.MAX_VALUE; // unlimited59 60 protected String cacheDirBase;61 62 protected final Map<TileSource, File> sourceCacheDirMap;63 64 65 public static File getDefaultCacheDir() throws SecurityException {66 String tempDir = null;67 String userName = System.getProperty("user.name");68 try {69 tempDir = System.getProperty("java.io.tmpdir");70 } catch (SecurityException e) {71 log.log(Level.WARNING,72 "Failed to access system property ''java.io.tmpdir'' for security reasons. Exception was: "73 + e.toString());74 throw e; // rethrow75 }76 try {77 if (tempDir == null)78 throw new IOException("No temp directory set");79 String subDirName = "JMapViewerTiles";80 // On Linux/Unix systems we do not have a per user tmp directory.81 // Therefore we add the user name for getting a unique dir name.82 if (userName != null && userName.length() > 0) {83 subDirName += "_" + userName;84 }85 File cacheDir = new File(tempDir, subDirName);86 return cacheDir;87 } catch (Exception e) {88 }89 return null;90 }91 92 /**93 * Create a OSMFileCacheTileLoader with given cache directory.94 * If cacheDir is not set or invalid, IOException will be thrown.95 * @param map the listener checking for tile load events (usually the map for display)96 * @param cacheDir directory to store cached tiles97 */98 public OsmFileCacheTileLoader(TileLoaderListener map, File cacheDir) throws IOException {99 super(map);100 if (cacheDir == null || (!cacheDir.exists() && !cacheDir.mkdirs()))101 throw new IOException("Cannot access cache directory");102 103 log.finest("Tile cache directory: " + cacheDir);104 cacheDirBase = cacheDir.getAbsolutePath();105 sourceCacheDirMap = new HashMap<>();106 }107 108 /**109 * Create a OSMFileCacheTileLoader with system property temp dir.110 * If not set an IOException will be thrown.111 * @param map the listener checking for tile load events (usually the map for display)112 */113 public OsmFileCacheTileLoader(TileLoaderListener map) throws SecurityException, IOException {114 this(map, getDefaultCacheDir());115 }116 117 @Override118 public TileJob createTileLoaderJob(final Tile tile) {119 return new FileLoadJob(tile);120 }121 122 protected File getSourceCacheDir(TileSource source) {123 File dir = sourceCacheDirMap.get(source);124 if (dir == null) {125 dir = new File(cacheDirBase, source.getName().replaceAll("[\\\\/:*?\"<>|]", "_"));126 if (!dir.exists()) {127 dir.mkdirs();128 }129 }130 return dir;131 }132 133 protected class FileLoadJob implements TileJob {134 InputStream input = null;135 136 Tile tile;137 File tileCacheDir;138 File tileFile = null;139 File tagsFile = null;140 Long fileMtime = null;141 Long now = null; // current time in milliseconds (keep consistent value for the whole run)142 143 public FileLoadJob(Tile tile) {144 this.tile = tile;145 }146 147 @Override148 public Tile getTile() {149 return tile;150 }151 152 @Override153 public void run() {154 synchronized (tile) {155 if ((tile.isLoaded() && !tile.hasError()) || tile.isLoading())156 return;157 tile.loaded = false;158 tile.error = false;159 tile.loading = true;160 }161 now = System.currentTimeMillis();162 tileCacheDir = getSourceCacheDir(tile.getSource());163 tileFile = getTileFile();164 tagsFile = getTagsFile();165 166 loadTagsFromFile();167 168 if (isCacheValid() && (isNoTileAtZoom() || loadTileFromFile())) {169 log.log(Level.FINE, "TMS - found in tile cache: {0}", tile);170 tile.setLoaded(true);171 listener.tileLoadingFinished(tile, true);172 return;173 }174 175 TileJob job = new TileJob() {176 177 @Override178 public void run() {179 if (loadOrUpdateTile()) {180 tile.setLoaded(true);181 listener.tileLoadingFinished(tile, true);182 } else {183 // failed to download - use old cache file if available184 if (isNoTileAtZoom() || loadTileFromFile()) {185 tile.setLoaded(true);186 tile.error = false;187 listener.tileLoadingFinished(tile, true);188 log.log(Level.FINE, "TMS - found stale tile in cache: {0}", tile);189 } else {190 // failed completely191 tile.setLoaded(true);192 listener.tileLoadingFinished(tile, false);193 }194 }195 }196 @Override197 public Tile getTile() {198 return tile;199 }200 };201 JobDispatcher.getInstance().addJob(job);202 }203 204 protected boolean loadOrUpdateTile() {205 try {206 URLConnection urlConn = loadTileFromOsm(tile);207 if (fileMtime != null && now - fileMtime <= ABSOLUTE_EXPIRE_TIME_LIMIT) {208 switch (tile.getSource().getTileUpdate()) {209 case IfModifiedSince:210 urlConn.setIfModifiedSince(fileMtime);211 break;212 case LastModified:213 if (!isOsmTileNewer(fileMtime)) {214 log.log(Level.FINE, "TMS - LastModified test: local version is up to date: {0}", tile);215 tileFile.setLastModified(now);216 return true;217 }218 break;219 default:220 break;221 }222 }223 if (tile.getSource().getTileUpdate() == TileUpdate.ETag || tile.getSource().getTileUpdate() == TileUpdate.IfNoneMatch) {224 String fileETag = tile.getValue("etag");225 if (fileETag != null) {226 switch (tile.getSource().getTileUpdate()) {227 case IfNoneMatch:228 urlConn.addRequestProperty("If-None-Match", fileETag);229 break;230 case ETag:231 if (hasOsmTileETag(fileETag)) {232 log.log(Level.FINE, "TMS - ETag test: local version is up to date: {0}", tile);233 tileFile.setLastModified(now);234 return true;235 }236 default:237 break;238 }239 }240 tile.putValue("etag", urlConn.getHeaderField("ETag"));241 }242 if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 304) {243 // If isModifiedSince or If-None-Match has been set244 // and the server answers with a HTTP 304 = "Not Modified"245 switch (tile.getSource().getTileUpdate()) {246 case IfModifiedSince:247 log.log(Level.FINE, "TMS - IfModifiedSince test: local version is up to date: {0}", tile);248 break;249 case IfNoneMatch:250 log.log(Level.FINE, "TMS - IfNoneMatch test: local version is up to date: {0}", tile);251 break;252 default:253 break;254 }255 loadTileFromFile();256 tileFile.setLastModified(now);257 return true;258 }259 260 loadTileMetadata(tile, urlConn);261 saveTagsToFile();262 263 if ("no-tile".equals(tile.getValue("tile-info")))264 {265 log.log(Level.FINE, "TMS - No tile: tile-info=no-tile: {0}", tile);266 tile.setError("No tile at this zoom level");267 return true;268 } else {269 for (int i = 0; i < 5; ++i) {270 if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 503) {271 Thread.sleep(5000+(new Random()).nextInt(5000));272 continue;273 }274 byte[] buffer = loadTileInBuffer(urlConn);275 if (buffer != null) {276 tile.loadImage(new ByteArrayInputStream(buffer));277 saveTileToFile(buffer);278 log.log(Level.FINE, "TMS - downloaded tile from server: {0}", tile.getUrl());279 return true;280 }281 }282 }283 } catch (Exception e) {284 tile.setError(e.getMessage());285 if (input == null) {286 try {287 log.log(Level.WARNING, "TMS - Failed downloading {0}: {1}", new Object[]{tile.getUrl(), e.getMessage()});288 return false;289 } catch(IOException i) {290 }291 }292 }293 log.log(Level.WARNING, "TMS - Failed downloading tile: {0}", tile);294 return false;295 }296 297 protected boolean isCacheValid() {298 Long expires = null;299 if (tileFile.exists()) {300 fileMtime = tileFile.lastModified();301 } else if (tagsFile.exists()) {302 fileMtime = tagsFile.lastModified();303 } else304 return false;305 306 try {307 expires = Long.parseLong(tile.getValue("expires"));308 } catch (NumberFormatException e) {}309 310 // check by expire date set by server311 if (expires != null && !expires.equals(0L)) {312 // put a limit to the expire time (some servers send a value313 // that is too large)314 expires = Math.min(expires, fileMtime + EXPIRE_TIME_SERVER_LIMIT);315 if (now > expires) {316 log.log(Level.FINE, "TMS - Tile has expired -> not valid {0}", tile);317 return false;318 }319 } else {320 // check by file modification date321 if (now - fileMtime > DEFAULT_EXPIRE_TIME) {322 log.log(Level.FINE, "TMS - Tile has expired, maximum file age reached {0}", tile);323 return false;324 }325 }326 return true;327 }328 329 protected boolean isNoTileAtZoom() {330 if ("no-tile".equals(tile.getValue("tile-info"))) {331 // do not remove file - keep the information, that there is no tile, for further requests332 // the code above will check, if this information is still valid333 log.log(Level.FINE, "TMS - Tile valid, but no file, as no tiles at this level {0}", tile);334 tile.setError("No tile at this zoom level");335 return true;336 }337 return false;338 }339 340 protected boolean loadTileFromFile() {341 if (!tileFile.exists())342 return false;343 344 try (FileInputStream fin = new FileInputStream(tileFile)) {345 if (fin.available() == 0)346 throw new IOException("File empty");347 tile.loadImage(fin);348 return true;349 } catch (Exception e) {350 log.log(Level.WARNING, "TMS - Error while loading image from tile cache: {0}; {1}", new Object[]{e.getMessage(), tile});351 tileFile.delete();352 if (tagsFile.exists()) {353 tagsFile.delete();354 }355 tileFile = null;356 fileMtime = null;357 }358 return false;359 }360 361 protected byte[] loadTileInBuffer(URLConnection urlConn) throws IOException {362 input = urlConn.getInputStream();363 try {364 ByteArrayOutputStream bout = new ByteArrayOutputStream(input.available());365 byte[] buffer = new byte[2048];366 boolean finished = false;367 do {368 int read = input.read(buffer);369 if (read >= 0) {370 bout.write(buffer, 0, read);371 } else {372 finished = true;373 }374 } while (!finished);375 if (bout.size() == 0)376 return null;377 return bout.toByteArray();378 } finally {379 input.close();380 input = null;381 }382 }383 384 /**385 * Performs a <code>HEAD</code> request for retrieving the386 * <code>LastModified</code> header value.387 *388 * Note: This does only work with servers providing the389 * <code>LastModified</code> header:390 * <ul>391 * <li>{@link org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource.CycleMap} - supported</li>392 * <li>{@link org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource.Mapnik} - not supported</li>393 * </ul>394 *395 * @param fileAge time of the396 * @return <code>true</code> if the tile on the server is newer than the397 * file398 * @throws IOException399 */400 protected boolean isOsmTileNewer(long fileAge) throws IOException {401 URL url;402 url = new URL(tile.getUrl());403 HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();404 prepareHttpUrlConnection(urlConn);405 urlConn.setRequestMethod("HEAD");406 urlConn.setReadTimeout(30000); // 30 seconds read timeout407 // System.out.println("Tile age: " + new408 // Date(urlConn.getLastModified()) + " / "409 // + new Date(fileMtime));410 long lastModified = urlConn.getLastModified();411 if (lastModified == 0)412 return true; // no LastModified time returned413 return (lastModified > fileAge);414 }415 416 protected boolean hasOsmTileETag(String eTag) throws IOException {417 URL url;418 url = new URL(tile.getUrl());419 HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();420 prepareHttpUrlConnection(urlConn);421 urlConn.setRequestMethod("HEAD");422 urlConn.setReadTimeout(30000); // 30 seconds read timeout423 // System.out.println("Tile age: " + new424 // Date(urlConn.getLastModified()) + " / "425 // + new Date(fileMtime));426 String osmETag = urlConn.getHeaderField("ETag");427 if (osmETag == null)428 return true;429 return (osmETag.equals(eTag));430 }431 432 protected File getTileFile() {433 return new File(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile() + "_" + tile.getYtile() + "."434 + tile.getSource().getTileType());435 }436 437 protected File getTagsFile() {438 return new File(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile() + "_" + tile.getYtile() + "."439 + TAGS_FILE_EXT);440 }441 442 protected void saveTileToFile(byte[] rawData) {443 File file = getTileFile();444 file.getParentFile().mkdirs();445 try (FileOutputStream f = new FileOutputStream(file)) {446 f.write(rawData);447 } catch (Exception e) {448 log.log(Level.SEVERE, "Failed to save tile content: {0}", e.getLocalizedMessage());449 }450 }451 452 protected void saveTagsToFile() {453 File tagsFile = getTagsFile();454 tagsFile.getParentFile().mkdirs();455 if (tile.getMetadata() == null) {456 tagsFile.delete();457 return;458 }459 try (PrintWriter f = new PrintWriter(new OutputStreamWriter(new FileOutputStream(tagsFile), TAGS_CHARSET))) {460 for (Entry<String, String> entry : tile.getMetadata().entrySet()) {461 f.println(entry.getKey() + "=" + entry.getValue());462 }463 } catch (Exception e) {464 System.err.println("Failed to save tile tags: " + e.getLocalizedMessage());465 }466 }467 468 protected boolean loadTagsFromFile() {469 File tagsFile = getTagsFile();470 try (BufferedReader f = new BufferedReader(new InputStreamReader(new FileInputStream(tagsFile), TAGS_CHARSET))) {471 for (String line = f.readLine(); line != null; line = f.readLine()) {472 final int i = line.indexOf('=');473 if (i == -1 || i == 0) {474 System.err.println("Malformed tile tag in file '" + tagsFile.getName() + "':" + line);475 continue;476 }477 tile.putValue(line.substring(0,i),line.substring(i+1));478 }479 } catch (FileNotFoundException e) {480 } catch (Exception e) {481 System.err.println("Failed to load tile tags: " + e.getLocalizedMessage());482 }483 484 return true;485 }486 }487 488 public String getCacheDirBase() {489 return cacheDirBase;490 }491 492 public void setTileCacheDir(String tileCacheDir) {493 File dir = new File(tileCacheDir);494 dir.mkdirs();495 this.cacheDirBase = dir.getAbsolutePath();496 }497 498 @Override499 public void clearCache(TileSource source) {500 clearCache(source, null);501 }502 503 @Override504 public void clearCache(TileSource source, TileClearController controller) {505 File dir = getSourceCacheDir(source);506 if (dir != null) {507 if (controller != null) controller.initClearDir(dir);508 if (dir.isDirectory()) {509 File[] files = dir.listFiles();510 if (controller != null) controller.initClearFiles(files);511 for (File file : files) {512 if (controller != null && controller.cancel()) return;513 file.delete();514 if (controller != null) controller.fileDeleted(file);515 }516 }517 dir.delete();518 }519 if (controller != null) controller.clearFinished();520 }521 } -
src/org/openstreetmap/gui/jmapviewer/interfaces/TileJob.java
17 17 * @return {@link Tile} to be handled 18 18 */ 19 19 public Tile getTile(); 20 21 /** 22 * submits download job to backend. 23 */ 24 void submit(); 20 25 } -
src/org/openstreetmap/gui/jmapviewer/interfaces/CachedTileLoader.java
5 5 * Interface that allow cleaning the tile cache without specifying exact type of loader 6 6 */ 7 7 public interface CachedTileLoader { 8 public void clearCache(TileSource source); 9 public void clearCache(TileSource source, TileClearController controller); 8 public void clearCache(); 10 9 } -
src/org/openstreetmap/gui/jmapviewer/Demo.java
10 10 import java.awt.event.ItemListener; 11 11 import java.awt.event.MouseAdapter; 12 12 import java.awt.event.MouseEvent; 13 import java.io.IOException;14 13 15 14 import javax.swing.JButton; 16 15 import javax.swing.JCheckBox; … … 101 100 } 102 101 }); 103 102 JComboBox<TileLoader> tileLoaderSelector; 104 try { 105 tileLoaderSelector = new JComboBox<>(new TileLoader[] { new OsmFileCacheTileLoader(map()), new OsmTileLoader(map()) }); 106 } catch (IOException e) { 107 tileLoaderSelector = new JComboBox<>(new TileLoader[] { new OsmTileLoader(map()) }); 108 } 103 tileLoaderSelector = new JComboBox<>(new TileLoader[] { new OsmTileLoader(map()) }); 109 104 tileLoaderSelector.addItemListener(new ItemListener() { 110 105 public void itemStateChanged(ItemEvent e) { 111 106 map().setTileLoader((TileLoader) e.getItem()); -
src/org/openstreetmap/gui/jmapviewer/TileController.java
45 45 tile.loadPlaceholderFromCache(tileCache); 46 46 } 47 47 if (!tile.isLoaded()) { 48 jobDispatcher.addJob(tileLoader.createTileLoaderJob(tile));48 tileLoader.createTileLoaderJob(tile).submit(); 49 49 } 50 50 return tile; 51 51 } -
src/org/openstreetmap/gui/jmapviewer/JMapViewer.java
985 985 } 986 986 987 987 public void tileLoadingFinished(Tile tile, boolean success) { 988 tile.setLoaded(success); 988 989 repaint(); 989 990 } 990 991 -
src/org/openstreetmap/gui/jmapviewer/OsmTileLoader.java
85 85 public Tile getTile() { 86 86 return tile; 87 87 } 88 89 @Override 90 public void submit() { 91 run(); 92 93 } 88 94 }; 89 95 } 90 96
