001// License: GPL. For details, see Readme.txt file. 002package org.openstreetmap.gui.jmapviewer; 003 004import static org.openstreetmap.gui.jmapviewer.FeatureAdapter.tr; 005 006import java.io.IOException; 007import java.io.InputStream; 008import java.net.HttpURLConnection; 009import java.net.URL; 010import java.net.URLConnection; 011import java.util.HashMap; 012import java.util.Map; 013import java.util.Map.Entry; 014import java.util.concurrent.Executors; 015import java.util.concurrent.ThreadPoolExecutor; 016import java.util.logging.Level; 017import java.util.logging.Logger; 018 019import org.openstreetmap.gui.jmapviewer.interfaces.TileJob; 020import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 021import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 022 023/** 024 * A {@link TileLoader} implementation that loads tiles from OSM. 025 * 026 * @author Jan Peter Stotz 027 */ 028public class OsmTileLoader implements TileLoader { 029 030 private static final Logger LOG = FeatureAdapter.getLogger(OsmTileLoader.class); 031 032 /** Setting key for number of threads */ 033 public static final String THREADS_SETTING = "jmapviewer.osm-tile-loader.threads"; 034 private static final int DEFAULT_THREADS_NUMBER = 8; 035 private static int nThreads = DEFAULT_THREADS_NUMBER; 036 static { 037 try { 038 nThreads = FeatureAdapter.getIntSetting(THREADS_SETTING, DEFAULT_THREADS_NUMBER); 039 } catch (Exception e) { 040 LOG.log(Level.SEVERE, e.getMessage(), e); 041 } 042 } 043 044 private static final ThreadPoolExecutor jobDispatcher = (ThreadPoolExecutor) Executors.newFixedThreadPool(nThreads); 045 046 private final class OsmTileJob implements TileJob { 047 private final Tile tile; 048 private InputStream input; 049 private boolean force; 050 051 private OsmTileJob(Tile tile) { 052 this.tile = tile; 053 } 054 055 @Override 056 public void run() { 057 synchronized (tile) { 058 if ((tile.isLoaded() && !tile.hasError()) || tile.isLoading()) 059 return; 060 tile.loaded = false; 061 tile.error = false; 062 tile.loading = true; 063 } 064 try { 065 URLConnection conn = loadTileFromOsm(tile); 066 if (force) { 067 conn.setUseCaches(false); 068 } 069 loadTileMetadata(tile, conn); 070 if ("no-tile".equals(tile.getValue("tile-info"))) { 071 tile.setError(tr("No tiles at this zoom level")); 072 } else { 073 input = conn.getInputStream(); 074 try { 075 tile.loadImage(input); 076 } finally { 077 input.close(); 078 input = null; 079 } 080 } 081 tile.setLoaded(true); 082 listener.tileLoadingFinished(tile, true); 083 } catch (IOException e) { 084 tile.setError(e.getMessage()); 085 listener.tileLoadingFinished(tile, false); 086 if (input == null) { 087 try { 088 LOG.log(Level.SEVERE, "Failed loading " + tile.getUrl() +": " 089 +e.getClass() + ": " + e.getMessage()); 090 } catch (IOException ioe) { 091 LOG.log(Level.SEVERE, ioe.getMessage(), ioe); 092 } 093 } 094 } finally { 095 tile.loading = false; 096 tile.setLoaded(true); 097 } 098 } 099 100 @Override 101 public void submit() { 102 submit(false); 103 } 104 105 @Override 106 public void submit(boolean force) { 107 this.force = force; 108 jobDispatcher.execute(this); 109 } 110 } 111 112 /** 113 * Holds the HTTP headers. Insert e.g. User-Agent here when default should not be used. 114 */ 115 public Map<String, String> headers = new HashMap<>(); 116 117 public int timeoutConnect; 118 public int timeoutRead; 119 120 protected TileLoaderListener listener; 121 122 /** 123 * Constructs a new {@code OsmTileLoader}. 124 * @param listener tile loader listener 125 */ 126 public OsmTileLoader(TileLoaderListener listener) { 127 this(listener, null); 128 } 129 130 public OsmTileLoader(TileLoaderListener listener, Map<String, String> headers) { 131 this.headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*"); 132 this.headers.put("User-Agent", "JMapViewer Java/"+System.getProperty("java.version")); 133 if (headers != null) { 134 this.headers.putAll(headers); 135 } 136 this.listener = listener; 137 } 138 139 @Override 140 public TileJob createTileLoaderJob(final Tile tile) { 141 return new OsmTileJob(tile); 142 } 143 144 protected URLConnection loadTileFromOsm(Tile tile) throws IOException { 145 URL url; 146 url = new URL(tile.getUrl()); 147 URLConnection urlConn = url.openConnection(); 148 if (urlConn instanceof HttpURLConnection) { 149 prepareHttpUrlConnection((HttpURLConnection) urlConn); 150 } 151 return urlConn; 152 } 153 154 protected void loadTileMetadata(Tile tile, URLConnection urlConn) { 155 String str = urlConn.getHeaderField("X-VE-TILEMETA-CaptureDatesRange"); 156 if (str != null) { 157 tile.putValue("capture-date", str); 158 } 159 str = urlConn.getHeaderField("X-VE-Tile-Info"); 160 if (str != null) { 161 tile.putValue("tile-info", str); 162 } 163 164 Long lng = urlConn.getExpiration(); 165 if (lng.equals(0L)) { 166 try { 167 str = urlConn.getHeaderField("Cache-Control"); 168 if (str != null) { 169 for (String token: str.split(",")) { 170 if (token.startsWith("max-age=")) { 171 lng = Long.parseLong(token.substring(8)) * 1000 + 172 System.currentTimeMillis(); 173 } 174 } 175 } 176 } catch (NumberFormatException e) { 177 // ignore malformed Cache-Control headers 178 if (JMapViewer.debug) { 179 System.err.println(e.getMessage()); 180 } 181 } 182 } 183 if (!lng.equals(0L)) { 184 tile.putValue("expires", lng.toString()); 185 } 186 } 187 188 protected void prepareHttpUrlConnection(HttpURLConnection urlConn) { 189 for (Entry<String, String> e : headers.entrySet()) { 190 urlConn.setRequestProperty(e.getKey(), e.getValue()); 191 } 192 if (timeoutConnect != 0) 193 urlConn.setConnectTimeout(timeoutConnect); 194 if (timeoutRead != 0) 195 urlConn.setReadTimeout(timeoutRead); 196 } 197 198 @Override 199 public String toString() { 200 return getClass().getSimpleName(); 201 } 202 203 @Override 204 public boolean hasOutstandingTasks() { 205 return jobDispatcher.getTaskCount() > jobDispatcher.getCompletedTaskCount(); 206 } 207 208 @Override 209 public void cancelOutstandingTasks() { 210 jobDispatcher.getQueue().clear(); 211 } 212 213 /** 214 * Sets the maximum number of concurrent connections the tile loader will do 215 * @param num number of concurrent connections 216 */ 217 public static void setConcurrentConnections(int num) { 218 jobDispatcher.setMaximumPoolSize(num); 219 } 220}