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