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