458 lines
18 KiB
Java
458 lines
18 KiB
Java
package org.openstreetmap.gui.jmapviewer;
|
|
|
|
//License: GPL. Copyright 2008 by Jan Peter Stotz
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.io.OutputStreamWriter;
|
|
import java.io.PrintWriter;
|
|
import java.net.HttpURLConnection;
|
|
import java.net.URL;
|
|
import java.net.URLConnection;
|
|
import java.nio.charset.Charset;
|
|
import java.util.Map.Entry;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
|
|
import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
|
|
import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
|
|
import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
|
|
import org.openstreetmap.gui.jmapviewer.interfaces.TileSource.TileUpdate;
|
|
|
|
/**
|
|
* A {@link TileLoader} implementation that loads tiles from OSM via HTTP and
|
|
* saves all loaded files in a directory located in the the temporary directory.
|
|
* If a tile is present in this file cache it will not be loaded from OSM again.
|
|
*
|
|
* @author Jan Peter Stotz
|
|
* @author Stefan Zeller
|
|
*/
|
|
public class OsmFileCacheTileLoader extends OsmTileLoader {
|
|
|
|
private static final Logger log = Logger.getLogger(OsmFileCacheTileLoader.class.getName());
|
|
|
|
private static final String ETAG_FILE_EXT = ".etag";
|
|
private static final String TAGS_FILE_EXT = ".tags";
|
|
|
|
private static final Charset TAGS_CHARSET = Charset.forName("UTF-8");
|
|
|
|
public static final long FILE_AGE_ONE_DAY = 1000 * 60 * 60 * 24;
|
|
public static final long FILE_AGE_ONE_WEEK = FILE_AGE_ONE_DAY * 7;
|
|
|
|
protected String cacheDirBase;
|
|
|
|
protected long maxCacheFileAge = FILE_AGE_ONE_WEEK;
|
|
protected long recheckAfter = FILE_AGE_ONE_DAY;
|
|
|
|
public static File getDefaultCacheDir() throws SecurityException {
|
|
String tempDir = null;
|
|
String userName = System.getProperty("user.name");
|
|
try {
|
|
tempDir = System.getProperty("java.io.tmpdir");
|
|
} catch (SecurityException e) {
|
|
log.log(Level.WARNING,
|
|
"Failed to access system property ''java.io.tmpdir'' for security reasons. Exception was: "
|
|
+ e.toString());
|
|
throw e; // rethrow
|
|
}
|
|
try {
|
|
if (tempDir == null)
|
|
throw new IOException("No temp directory set");
|
|
String subDirName = "JMapViewerTiles";
|
|
// On Linux/Unix systems we do not have a per user tmp directory.
|
|
// Therefore we add the user name for getting a unique dir name.
|
|
if (userName != null && userName.length() > 0) {
|
|
subDirName += "_" + userName;
|
|
}
|
|
File cacheDir = new File(tempDir, subDirName);
|
|
return cacheDir;
|
|
} catch (Exception e) {
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Create a OSMFileCacheTileLoader with given cache directory.
|
|
* If cacheDir is not set or invalid, IOException will be thrown.
|
|
* @param map
|
|
* @param cacheDir
|
|
*/
|
|
public OsmFileCacheTileLoader(TileLoaderListener map, File cacheDir) throws IOException {
|
|
super(map);
|
|
if (cacheDir == null || (!cacheDir.exists() && !cacheDir.mkdirs()))
|
|
throw new IOException("Cannot access cache directory");
|
|
|
|
log.finest("Tile cache directory: " + cacheDir);
|
|
cacheDirBase = cacheDir.getAbsolutePath();
|
|
}
|
|
|
|
/**
|
|
* Create a OSMFileCacheTileLoader with system property temp dir.
|
|
* If not set an IOException will be thrown.
|
|
* @param map
|
|
*/
|
|
public OsmFileCacheTileLoader(TileLoaderListener map) throws SecurityException, IOException {
|
|
this(map, getDefaultCacheDir());
|
|
}
|
|
|
|
@Override
|
|
public Runnable createTileLoaderJob(final TileSource source, final int tilex, final int tiley, final int zoom) {
|
|
return new FileLoadJob(source, tilex, tiley, zoom);
|
|
}
|
|
|
|
protected class FileLoadJob implements Runnable {
|
|
InputStream input = null;
|
|
|
|
int tilex, tiley, zoom;
|
|
Tile tile;
|
|
TileSource source;
|
|
File tileCacheDir;
|
|
File tileFile = null;
|
|
long fileAge = 0;
|
|
boolean fileTilePainted = false;
|
|
|
|
public FileLoadJob(TileSource source, int tilex, int tiley, int zoom) {
|
|
this.source = source;
|
|
this.tilex = tilex;
|
|
this.tiley = tiley;
|
|
this.zoom = zoom;
|
|
}
|
|
|
|
public void run() {
|
|
TileCache cache = listener.getTileCache();
|
|
synchronized (cache) {
|
|
tile = cache.getTile(source, tilex, tiley, zoom);
|
|
if (tile == null || tile.isLoaded() || tile.loading)
|
|
return;
|
|
tile.loading = true;
|
|
}
|
|
tileCacheDir = new File(cacheDirBase, source.getName().replaceAll("[\\\\/:*?\"<>|]", "_"));
|
|
if (!tileCacheDir.exists()) {
|
|
tileCacheDir.mkdirs();
|
|
}
|
|
if (loadTileFromFile())
|
|
return;
|
|
if (fileTilePainted) {
|
|
Runnable job = new Runnable() {
|
|
|
|
public void run() {
|
|
loadOrUpdateTile();
|
|
}
|
|
};
|
|
JobDispatcher.getInstance().addJob(job);
|
|
} else {
|
|
loadOrUpdateTile();
|
|
}
|
|
}
|
|
|
|
protected void loadOrUpdateTile() {
|
|
|
|
try {
|
|
// log.finest("Loading tile from OSM: " + tile);
|
|
URLConnection urlConn = loadTileFromOsm(tile);
|
|
if (tileFile != null) {
|
|
switch (source.getTileUpdate()) {
|
|
case IfModifiedSince:
|
|
urlConn.setIfModifiedSince(fileAge);
|
|
break;
|
|
case LastModified:
|
|
if (!isOsmTileNewer(fileAge)) {
|
|
log.finest("LastModified test: local version is up to date: " + tile);
|
|
tile.setLoaded(true);
|
|
tileFile.setLastModified(System.currentTimeMillis() - maxCacheFileAge + recheckAfter);
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (source.getTileUpdate() == TileUpdate.ETag || source.getTileUpdate() == TileUpdate.IfNoneMatch) {
|
|
String fileETag = tile.getValue("etag");
|
|
if (fileETag != null) {
|
|
switch (source.getTileUpdate()) {
|
|
case IfNoneMatch:
|
|
urlConn.addRequestProperty("If-None-Match", fileETag);
|
|
break;
|
|
case ETag:
|
|
if (hasOsmTileETag(fileETag)) {
|
|
tile.setLoaded(true);
|
|
tileFile.setLastModified(System.currentTimeMillis() - maxCacheFileAge
|
|
+ recheckAfter);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
tile.putValue("etag", urlConn.getHeaderField("ETag"));
|
|
}
|
|
if (urlConn instanceof HttpURLConnection && ((HttpURLConnection)urlConn).getResponseCode() == 304) {
|
|
// If we are isModifiedSince or If-None-Match has been set
|
|
// and the server answers with a HTTP 304 = "Not Modified"
|
|
log.finest("ETag test: local version is up to date: " + tile);
|
|
tile.setLoaded(true);
|
|
tileFile.setLastModified(System.currentTimeMillis() - maxCacheFileAge + recheckAfter);
|
|
return;
|
|
}
|
|
|
|
loadTileMetadata(tile, urlConn);
|
|
saveTagsToFile();
|
|
|
|
if ("no-tile".equals(tile.getValue("tile-info")))
|
|
{
|
|
tile.setError("No tile at this zoom level");
|
|
listener.tileLoadingFinished(tile, true);
|
|
} else {
|
|
byte[] buffer = loadTileInBuffer(urlConn);
|
|
if (buffer != null) {
|
|
tile.loadImage(new ByteArrayInputStream(buffer));
|
|
tile.setLoaded(true);
|
|
listener.tileLoadingFinished(tile, true);
|
|
saveTileToFile(buffer);
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
tile.setError(e.getMessage());
|
|
listener.tileLoadingFinished(tile, false);
|
|
if (input == null) {
|
|
System.err.println("failed loading " + zoom + "/" + tilex + "/" + tiley + " " + e.getMessage());
|
|
}
|
|
} finally {
|
|
tile.loading = false;
|
|
tile.setLoaded(true);
|
|
}
|
|
}
|
|
|
|
protected boolean loadTileFromFile() {
|
|
FileInputStream fin = null;
|
|
try {
|
|
tileFile = getTileFile();
|
|
loadTagsFromFile();
|
|
if ("no-tile".equals(tile.getValue("tile-info")))
|
|
{
|
|
tile.setError("No tile at this zoom level");
|
|
if (tileFile.exists()) {
|
|
tileFile.delete();
|
|
}
|
|
tileFile = getTagsFile();
|
|
} else {
|
|
fin = new FileInputStream(tileFile);
|
|
if (fin.available() == 0)
|
|
throw new IOException("File empty");
|
|
tile.loadImage(fin);
|
|
fin.close();
|
|
}
|
|
|
|
fileAge = tileFile.lastModified();
|
|
boolean oldTile = System.currentTimeMillis() - fileAge > maxCacheFileAge;
|
|
// System.out.println("Loaded from file: " + tile);
|
|
if (!oldTile) {
|
|
tile.setLoaded(true);
|
|
listener.tileLoadingFinished(tile, true);
|
|
fileTilePainted = true;
|
|
return true;
|
|
}
|
|
listener.tileLoadingFinished(tile, true);
|
|
fileTilePainted = true;
|
|
} catch (Exception e) {
|
|
try {
|
|
if (fin != null) {
|
|
fin.close();
|
|
tileFile.delete();
|
|
}
|
|
} catch (Exception e1) {
|
|
}
|
|
tileFile = null;
|
|
fileAge = 0;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected byte[] loadTileInBuffer(URLConnection urlConn) throws IOException {
|
|
input = urlConn.getInputStream();
|
|
ByteArrayOutputStream bout = new ByteArrayOutputStream(input.available());
|
|
byte[] buffer = new byte[2048];
|
|
boolean finished = false;
|
|
do {
|
|
int read = input.read(buffer);
|
|
if (read >= 0) {
|
|
bout.write(buffer, 0, read);
|
|
} else {
|
|
finished = true;
|
|
}
|
|
} while (!finished);
|
|
if (bout.size() == 0)
|
|
return null;
|
|
return bout.toByteArray();
|
|
}
|
|
|
|
/**
|
|
* Performs a <code>HEAD</code> request for retrieving the
|
|
* <code>LastModified</code> header value.
|
|
*
|
|
* Note: This does only work with servers providing the
|
|
* <code>LastModified</code> header:
|
|
* <ul>
|
|
* <li>{@link OsmTileLoader#MAP_OSMA} - supported</li>
|
|
* <li>{@link OsmTileLoader#MAP_MAPNIK} - not supported</li>
|
|
* </ul>
|
|
*
|
|
* @param fileAge
|
|
* @return <code>true</code> if the tile on the server is newer than the
|
|
* file
|
|
* @throws IOException
|
|
*/
|
|
protected boolean isOsmTileNewer(long fileAge) throws IOException {
|
|
URL url;
|
|
url = new URL(tile.getUrl());
|
|
HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
|
|
prepareHttpUrlConnection(urlConn);
|
|
urlConn.setRequestMethod("HEAD");
|
|
urlConn.setReadTimeout(30000); // 30 seconds read timeout
|
|
// System.out.println("Tile age: " + new
|
|
// Date(urlConn.getLastModified()) + " / "
|
|
// + new Date(fileAge));
|
|
long lastModified = urlConn.getLastModified();
|
|
if (lastModified == 0)
|
|
return true; // no LastModified time returned
|
|
return (lastModified > fileAge);
|
|
}
|
|
|
|
protected boolean hasOsmTileETag(String eTag) throws IOException {
|
|
URL url;
|
|
url = new URL(tile.getUrl());
|
|
HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
|
|
prepareHttpUrlConnection(urlConn);
|
|
urlConn.setRequestMethod("HEAD");
|
|
urlConn.setReadTimeout(30000); // 30 seconds read timeout
|
|
// System.out.println("Tile age: " + new
|
|
// Date(urlConn.getLastModified()) + " / "
|
|
// + new Date(fileAge));
|
|
String osmETag = urlConn.getHeaderField("ETag");
|
|
if (osmETag == null)
|
|
return true;
|
|
return (osmETag.equals(eTag));
|
|
}
|
|
|
|
protected File getTileFile() {
|
|
return new File(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile() + "_" + tile.getYtile() + "."
|
|
+ source.getTileType());
|
|
}
|
|
|
|
protected File getTagsFile() {
|
|
return new File(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile() + "_" + tile.getYtile()
|
|
+ TAGS_FILE_EXT);
|
|
}
|
|
|
|
protected void saveTileToFile(byte[] rawData) {
|
|
try {
|
|
FileOutputStream f = new FileOutputStream(tileCacheDir + "/" + tile.getZoom() + "_" + tile.getXtile()
|
|
+ "_" + tile.getYtile() + "." + source.getTileType());
|
|
f.write(rawData);
|
|
f.close();
|
|
// System.out.println("Saved tile to file: " + tile);
|
|
} catch (Exception e) {
|
|
System.err.println("Failed to save tile content: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
protected void saveTagsToFile() {
|
|
File tagsFile = getTagsFile();
|
|
if (tile.getMetadata() == null) {
|
|
tagsFile.delete();
|
|
return;
|
|
}
|
|
try {
|
|
final PrintWriter f = new PrintWriter(new OutputStreamWriter(new FileOutputStream(tagsFile),
|
|
TAGS_CHARSET));
|
|
for (Entry<String, String> entry : tile.getMetadata().entrySet()) {
|
|
f.println(entry.getKey() + "=" + entry.getValue());
|
|
}
|
|
f.close();
|
|
} catch (Exception e) {
|
|
System.err.println("Failed to save tile tags: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
/** Load backward-compatiblity .etag file and if it exists move it to new .tags file*/
|
|
private void loadOldETagfromFile() {
|
|
File etagFile = new File(tileCacheDir, tile.getZoom() + "_"
|
|
+ tile.getXtile() + "_" + tile.getYtile() + ETAG_FILE_EXT);
|
|
if (!etagFile.exists()) return;
|
|
try {
|
|
FileInputStream f = new FileInputStream(etagFile);
|
|
byte[] buf = new byte[f.available()];
|
|
f.read(buf);
|
|
f.close();
|
|
String etag = new String(buf, TAGS_CHARSET.name());
|
|
tile.putValue("etag", etag);
|
|
if (etagFile.delete()) {
|
|
saveTagsToFile();
|
|
}
|
|
} catch (IOException e) {
|
|
System.err.println("Failed to load compatiblity etag: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
protected void loadTagsFromFile() {
|
|
loadOldETagfromFile();
|
|
File tagsFile = getTagsFile();
|
|
try {
|
|
final BufferedReader f = new BufferedReader(new InputStreamReader(new FileInputStream(tagsFile),
|
|
TAGS_CHARSET));
|
|
for (String line = f.readLine(); line != null; line = f.readLine()) {
|
|
final int i = line.indexOf('=');
|
|
if (i == -1 || i == 0) {
|
|
System.err.println("Malformed tile tag in file '" + tagsFile.getName() + "':" + line);
|
|
continue;
|
|
}
|
|
tile.putValue(line.substring(0,i),line.substring(i+1));
|
|
}
|
|
f.close();
|
|
} catch (FileNotFoundException e) {
|
|
} catch (Exception e) {
|
|
System.err.println("Failed to load tile tags: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public long getMaxFileAge() {
|
|
return maxCacheFileAge;
|
|
}
|
|
|
|
/**
|
|
* Sets the maximum age of the local cached tile in the file system. If a
|
|
* local tile is older than the specified file age
|
|
* {@link OsmFileCacheTileLoader} will connect to the tile server and check
|
|
* if a newer tile is available using the mechanism specified for the
|
|
* selected tile source/server.
|
|
*
|
|
* @param maxFileAge
|
|
* maximum age in milliseconds
|
|
* @see #FILE_AGE_ONE_DAY
|
|
* @see #FILE_AGE_ONE_WEEK
|
|
* @see TileSource#getTileUpdate()
|
|
*/
|
|
public void setCacheMaxFileAge(long maxFileAge) {
|
|
this.maxCacheFileAge = maxFileAge;
|
|
}
|
|
|
|
public String getCacheDirBase() {
|
|
return cacheDirBase;
|
|
}
|
|
|
|
public void setTileCacheDir(String tileCacheDir) {
|
|
File dir = new File(tileCacheDir);
|
|
dir.mkdirs();
|
|
this.cacheDirBase = dir.getAbsolutePath();
|
|
}
|
|
|
|
}
|