// License: GPL. For details, see Readme.txt file.
package org.openstreetmap.gui.jmapviewer;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
/**
* A {@link TileLoader} implementation that loads tiles from OSM.
*
* @author Jan Peter Stotz
*/
public class OsmTileLoader implements TileLoader {
private static final ThreadPoolExecutor jobDispatcher = (ThreadPoolExecutor) Executors.newFixedThreadPool(3);
private final class OsmTileJob implements TileJob {
private final Tile tile;
private InputStream input;
private boolean force;
private OsmTileJob(Tile tile) {
this.tile = tile;
}
@Override
public void run() {
synchronized (tile) {
if ((tile.isLoaded() && !tile.hasError()) || tile.isLoading())
return;
tile.loaded = false;
tile.error = false;
tile.loading = true;
}
try {
URLConnection conn = loadTileFromOsm(tile);
if (force) {
conn.setUseCaches(false);
}
loadTileMetadata(tile, conn);
if ("no-tile".equals(tile.getValue("tile-info"))) {
tile.setError("No tile at this zoom level");
} else {
input = conn.getInputStream();
try {
tile.loadImage(input);
} finally {
input.close();
input = null;
}
}
tile.setLoaded(true);
listener.tileLoadingFinished(tile, true);
} catch (IOException e) {
tile.setError(e.getMessage());
listener.tileLoadingFinished(tile, false);
if (input == null) {
try {
System.err.println("Failed loading " + tile.getUrl() +": "
+e.getClass() + ": " + e.getMessage());
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
} finally {
tile.loading = false;
tile.setLoaded(true);
}
}
@Override
public void submit() {
submit(false);
}
@Override
public void submit(boolean force) {
this.force = force;
jobDispatcher.execute(this);
}
}
/**
* Holds the HTTP headers. Insert e.g. User-Agent here when default should not be used.
*/
public Map<String, String> headers = new HashMap<>();
public int timeoutConnect;
public int timeoutRead;
protected TileLoaderListener listener;
public OsmTileLoader(TileLoaderListener listener) {
this(listener, null);
}
public OsmTileLoader(TileLoaderListener listener, Map<String, String> headers) {
this.headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
if (headers != null) {
this.headers.putAll(headers);
}
this.listener = listener;
}
@Override
public TileJob createTileLoaderJob(final Tile tile) {
return new OsmTileJob(tile);
}
protected URLConnection loadTileFromOsm(Tile tile) throws IOException {
URL url;
url = new URL(tile.getUrl());
URLConnection urlConn = url.openConnection();
if (urlConn instanceof HttpURLConnection) {
prepareHttpUrlConnection((HttpURLConnection) urlConn);
}
return urlConn;
}
protected void loadTileMetadata(Tile tile, URLConnection urlConn) {
String str = urlConn.getHeaderField("X-VE-TILEMETA-CaptureDatesRange");
if (str != null) {
tile.putValue("capture-date", str);
}
str = urlConn.getHeaderField("X-VE-Tile-Info");
if (str != null) {
tile.putValue("tile-info", str);
}
Long lng = urlConn.getExpiration();
if (lng.equals(0L)) {
try {
str = urlConn.getHeaderField("Cache-Control");
if (str != null) {
for (String token: str.split(",")) {
if (token.startsWith("max-age=")) {
lng = Long.parseLong(token.substring(8)) * 1000 +
System.currentTimeMillis();
}
}
}
} catch (NumberFormatException e) {
// ignore malformed Cache-Control headers
if (JMapViewer.debug) {
System.err.println(e.getMessage());
}
}
}
if (!lng.equals(0L)) {
tile.putValue("expires", lng.toString());
}
}
protected void prepareHttpUrlConnection(HttpURLConnection urlConn) {
for (Entry<String, String> e : headers.entrySet()) {
urlConn.setRequestProperty(e.getKey(), e.getValue());
}
if (timeoutConnect != 0)
urlConn.setConnectTimeout(timeoutConnect);
if (timeoutRead != 0)
urlConn.setReadTimeout(timeoutRead);
}
@Override
public String toString() {
return getClass().getSimpleName();
}
@Override
public void cancelOutstandingTasks() {
jobDispatcher.getQueue().clear();
}
/**
* Sets the maximum number of concurrent connections the tile loader will do
* @param num number of conncurent connections
*/
public static void setConcurrentConnections(int num) {
jobDispatcher.setMaximumPoolSize(num);
}
}