diff --git a/vtm-android-example/src/org/oscim/android/test/BaseMapActivity.java b/vtm-android-example/src/org/oscim/android/test/BaseMapActivity.java index e2387bcf..ee7ba3f4 100644 --- a/vtm-android-example/src/org/oscim/android/test/BaseMapActivity.java +++ b/vtm-android-example/src/org/oscim/android/test/BaseMapActivity.java @@ -2,6 +2,7 @@ package org.oscim.android.test; import org.oscim.android.MapActivity; import org.oscim.android.MapView; +import org.oscim.android.cache.TileCache; import org.oscim.layers.tile.vector.VectorTileLayer; import org.oscim.tiling.source.TileSource; import org.oscim.tiling.source.oscimap4.OSciMap4TileSource; @@ -13,6 +14,9 @@ public class BaseMapActivity extends MapActivity { MapView mMapView; VectorTileLayer mBaseLayer; + TileSource mTileSource; + + private TileCache mCache; @Override public void onCreate(Bundle savedInstanceState) { @@ -21,10 +25,21 @@ public class BaseMapActivity extends MapActivity { mMapView = (MapView) findViewById(R.id.mapView); - TileSource tileSource = new OSciMap4TileSource(); - tileSource.setOption("url", "http://opensciencemap.org/tiles/vtm"); + mTileSource = new OSciMap4TileSource(); + mTileSource.setOption("url", "http://opensciencemap.org/tiles/vtm"); - mBaseLayer = mMap.setBaseMap(tileSource); + + mCache = new TileCache(this, "cachedir", "testdb"); + mCache.setCacheSize(512 * (1 << 10)); + mTileSource.setCache(mCache); + + mBaseLayer = mMap.setBaseMap(mTileSource); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mCache.dispose(); } @Override diff --git a/vtm-android/src/org/oscim/android/cache/TileCache.java b/vtm-android/src/org/oscim/android/cache/TileCache.java new file mode 100644 index 00000000..076af367 --- /dev/null +++ b/vtm-android/src/org/oscim/android/cache/TileCache.java @@ -0,0 +1,277 @@ +/* + * Copyright 2013 Hannes Janetzek + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.android.cache; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; + +import org.oscim.core.Tile; +import org.oscim.tiling.source.ITileCache; +import org.slf4j.LoggerFactory; + +import android.annotation.TargetApi; +import android.content.Context; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDoneException; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteStatement; +import android.os.Build; +import android.os.ParcelFileDescriptor; + +public class TileCache implements ITileCache { + + final static org.slf4j.Logger log = LoggerFactory.getLogger(TileCache.class); + + class CacheTileReader implements TileReader { + final InputStream mInputStream; + final Tile mTile; + final int mSize; + + public CacheTileReader(Tile tile, InputStream is, int size) { + mTile = tile; + mInputStream = is; + mSize = size; + } + + @Override + public Tile getTile() { + return mTile; + } + + @Override + public InputStream getInputStream() { + return mInputStream; + } + + @Override + public int getBytes() { + return mSize; + } + } + + class CacheTileWriter implements TileWriter { + final ByteArrayOutputStream mOutputStream; + final Tile mTile; + + CacheTileWriter(Tile tile, ByteArrayOutputStream os) { + mTile = tile; + mOutputStream = os; + } + + @Override + public Tile getTile() { + return mTile; + } + + @Override + public OutputStream getOutputStream() { + return mOutputStream; + } + + @Override + public void complete(boolean success) { + saveTile(mTile, mOutputStream, success); + } + } + + private final ArrayList mCacheBuffers; + + private final SQLiteHelper dbHelper; + + private final SQLiteDatabase mDatabase; + private final SQLiteStatement mStmtGetTile; + private final SQLiteStatement mStmtPutTile; + private final SQLiteStatement mStmtUpdateTile; + + public void dispose() { + if (mDatabase.isOpen()) + mDatabase.close(); + } + + public TileCache(Context context, String cacheDirectory, String dbName) { + + dbHelper = new SQLiteHelper(context); + mDatabase = dbHelper.getWritableDatabase(); + + mStmtGetTile = mDatabase.compileStatement("" + + "SELECT " + COLUMN_DATA + + " FROM " + TABLE_NAME + + " WHERE x=? AND y=? AND z = ?"); + + mStmtPutTile = mDatabase.compileStatement("" + + "INSERT INTO " + TABLE_NAME + + " (x, y, z, time, last_access, data)" + + " VALUES(?,?,?,?,?,?)"); + + mStmtUpdateTile = mDatabase.compileStatement("" + + "UPDATE " + TABLE_NAME + + " SET last_access=?" + + " WHERE x=? AND y=? AND z=?"); + + mCacheBuffers = new ArrayList(); + } + + @Override + public TileWriter writeTile(Tile tile) { + ByteArrayOutputStream os; + + synchronized (mCacheBuffers) { + if (mCacheBuffers.size() == 0) + os = new ByteArrayOutputStream(32 * 1024); + else + os = mCacheBuffers.remove(mCacheBuffers.size() - 1); + } + return new CacheTileWriter(tile, os); + } + + static final String TABLE_NAME = "tiles"; + static final String COLUMN_TIME = "time"; + static final String COLUMN_ACCESS = "last_access"; + static final String COLUMN_DATA = "data"; + + //static final String COLUMN_SIZE = "size"; + + class SQLiteHelper extends SQLiteOpenHelper { + + private static final String DATABASE_NAME = "tile.db"; + private static final int DATABASE_VERSION = 3; + + private static final String DATABASE_CREATE = + "CREATE TABLE " + + TABLE_NAME + "(" + + "x INTEGER NOT NULL," + + "y INTEGER NOT NULL," + + "z INTEGER NOT NULL," + + COLUMN_TIME + " LONG NOT NULL," + //+ COLUMN_SIZE + " LONG NOT NULL," + + COLUMN_ACCESS + " LONG NOT NULL," + + COLUMN_DATA + " BLOB," + + "PRIMARY KEY(x,y,z));"; + + public SQLiteHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(DATABASE_CREATE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); + onCreate(db); + } + } + + public void saveTile(Tile tile, ByteArrayOutputStream data, boolean success) { + byte[] bytes = null; + + if (success) + bytes = data.toByteArray(); + + synchronized (mCacheBuffers) { + data.reset(); + mCacheBuffers.add(data); + } + + log.debug("store tile {} {}", tile, Boolean.valueOf(success)); + + if (!success) + return; + + synchronized (mStmtPutTile) { + mStmtPutTile.bindLong(1, tile.tileX); + mStmtPutTile.bindLong(2, tile.tileY); + mStmtPutTile.bindLong(3, tile.zoomLevel); + mStmtPutTile.bindLong(4, 0); + mStmtPutTile.bindLong(5, 0); + mStmtPutTile.bindBlob(6, bytes); + + mStmtPutTile.execute(); + + mStmtPutTile.clearBindings(); + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public synchronized TileReader getTileApi11(Tile tile) { + InputStream in = null; + + mStmtGetTile.bindLong(1, tile.tileX); + mStmtGetTile.bindLong(2, tile.tileY); + mStmtGetTile.bindLong(3, tile.zoomLevel); + + try { + ParcelFileDescriptor result = mStmtGetTile.simpleQueryForBlobFileDescriptor(); + in = new FileInputStream(result.getFileDescriptor()); + } catch (SQLiteDoneException e) { + log.debug("not in cache {}", tile); + return null; + } finally { + mStmtGetTile.clearBindings(); + } + + log.debug("load tile {}", tile); + + return new CacheTileReader(tile, in, Integer.MAX_VALUE); + } + + private final String[] mQueryVals = new String[3]; + + @Override + public synchronized TileReader getTile(Tile tile) { + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) + return getTileApi11(tile); + + mQueryVals[0] = String.valueOf(tile.zoomLevel); + mQueryVals[1] = String.valueOf(tile.tileX); + mQueryVals[2] = String.valueOf(tile.tileY); + + Cursor cursor = mDatabase.rawQuery("SELECT " + COLUMN_DATA + + " FROM " + TABLE_NAME + + " WHERE z=? AND x=? AND y=?", mQueryVals); + + if (!cursor.moveToFirst()) { + log.debug("not in cache {}", tile); + return null; + } + + InputStream in = new ByteArrayInputStream(cursor.getBlob(0)); + + if (!cursor.isClosed()) + cursor.close(); + + log.debug("load tile {}", tile); + + return new CacheTileReader(tile, in, Integer.MAX_VALUE); + } + + public SQLiteDatabase open() throws SQLException { + return dbHelper.getWritableDatabase(); + } + + @Override + public void setCacheSize(long size) { + // TODO Auto-generated method stub + } +} diff --git a/vtm/src/org/oscim/tiling/source/TileSource.java b/vtm/src/org/oscim/tiling/source/TileSource.java index ef9c985d..129ae8e2 100644 --- a/vtm/src/org/oscim/tiling/source/TileSource.java +++ b/vtm/src/org/oscim/tiling/source/TileSource.java @@ -26,6 +26,12 @@ public abstract class TileSource { protected final Options options = new Options(); + public ITileCache tileCache; + + public void setCache(ITileCache cache) { + tileCache = cache; + } + public TileSource setOption(String key, String value) { options.put(key, value); return this; diff --git a/vtm/src/org/oscim/tiling/source/common/LwHttp.java b/vtm/src/org/oscim/tiling/source/common/LwHttp.java index 46e92355..258e1747 100644 --- a/vtm/src/org/oscim/tiling/source/common/LwHttp.java +++ b/vtm/src/org/oscim/tiling/source/common/LwHttp.java @@ -104,22 +104,40 @@ public class LwHttp { } static class Buffer extends BufferedInputStream { - public Buffer(InputStream is) { + final OutputStream mCache; + + public Buffer(InputStream is, OutputStream cache) { super(is, 4096); + mCache = cache; } @Override public synchronized int read() throws IOException { - return super.read(); + int data = super.read(); + if (data >= 0) + mCache.write(data); + + return data; } @Override public synchronized int read(byte[] buffer, int offset, int byteCount) throws IOException { - return super.read(buffer, offset, byteCount); + int len = super.read(buffer, offset, byteCount); + + if (len >= 0) + mCache.write(buffer, offset, len); + + return len; } } + OutputStream mCacheOutputStream; + + public void setOutputStream(OutputStream outputStream) { + mCacheOutputStream = outputStream; + } + public void close() { if (mSocket != null) { try { @@ -204,6 +222,10 @@ public class LwHttp { is.mark(0); is.skip(end); + if (mCacheOutputStream != null) { + is = new Buffer(is, mCacheOutputStream); + } + if (mInflateContent) return new InflaterInputStream(is); diff --git a/vtm/src/org/oscim/tiling/source/common/PbfTileDataSource.java b/vtm/src/org/oscim/tiling/source/common/PbfTileDataSource.java index 37e420fd..aeec26d5 100644 --- a/vtm/src/org/oscim/tiling/source/common/PbfTileDataSource.java +++ b/vtm/src/org/oscim/tiling/source/common/PbfTileDataSource.java @@ -14,14 +14,17 @@ */ package org.oscim.tiling.source.common; +import java.io.IOException; import java.io.InputStream; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import org.oscim.tiling.MapTile; +import org.oscim.tiling.source.ITileCache; import org.oscim.tiling.source.ITileDataSink; import org.oscim.tiling.source.ITileDataSource; +import org.oscim.utils.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,48 +37,76 @@ public abstract class PbfTileDataSource implements ITileDataSource { protected LwHttp mConn; protected final PbfDecoder mTileDecoder; + protected final ITileCache mTileCache; - public PbfTileDataSource(PbfDecoder tileDecoder) { + public PbfTileDataSource(PbfDecoder tileDecoder, ITileCache tileCache) { mTileDecoder = tileDecoder; + mTileCache = tileCache; } @Override public QueryResult executeQuery(MapTile tile, ITileDataSink sink) { - QueryResult result = QueryResult.SUCCESS; + boolean success = true; + + ITileCache.TileWriter cacheWriter = null; + + if (mTileCache != null) { + ITileCache.TileReader c = mTileCache.getTile(tile); + if (c == null) { + // create new cache entry + cacheWriter = mTileCache.writeTile(tile); + mConn.setOutputStream(cacheWriter.getOutputStream()); + } else { + try { + InputStream is = c.getInputStream(); + if (mTileDecoder.decode(tile, sink, is, c.getBytes())) { + IOUtils.closeQuietly(is); + return QueryResult.SUCCESS; + } + } catch (IOException e) { + e.printStackTrace(); + } + log.debug(tile + " Cache read failed"); + } + } try { InputStream is; if (!mConn.sendRequest(tile)) { - log.debug(tile + " Request Failed"); - result = QueryResult.FAILED; + log.debug(tile + " Request failed"); + success = false; } else if ((is = mConn.readHeader()) != null) { - boolean win = mTileDecoder.decode(tile, sink, is, mConn.getContentLength()); - if (!win) - log.debug(tile + " failed"); + int bytes = mConn.getContentLength(); + success = mTileDecoder.decode(tile, sink, is, bytes); + if (!success) + log.debug(tile + " Decoding failed"); } else { log.debug(tile + " Network Error"); - result = QueryResult.FAILED; + success = false; } } catch (SocketException e) { log.debug(tile + " Socket exception: " + e.getMessage()); - result = QueryResult.FAILED; + success = false; } catch (SocketTimeoutException e) { log.debug(tile + " Socket Timeout"); - result = QueryResult.FAILED; + success = false; } catch (UnknownHostException e) { log.debug(tile + " No Network"); - result = QueryResult.FAILED; + success = false; } catch (Exception e) { e.printStackTrace(); - result = QueryResult.FAILED; + success = false; } mConn.requestCompleted(); - if (result != QueryResult.SUCCESS) + if (cacheWriter != null) + cacheWriter.complete(success); + + if (success) mConn.close(); - return result; + return success ? QueryResult.SUCCESS : QueryResult.FAILED; } @Override diff --git a/vtm/src/org/oscim/tiling/source/mapnik/MapnikVectorTileSource.java b/vtm/src/org/oscim/tiling/source/mapnik/MapnikVectorTileSource.java index 482f1e84..520c257a 100644 --- a/vtm/src/org/oscim/tiling/source/mapnik/MapnikVectorTileSource.java +++ b/vtm/src/org/oscim/tiling/source/mapnik/MapnikVectorTileSource.java @@ -18,6 +18,7 @@ import java.net.URL; import org.oscim.core.Tile; import org.oscim.tiling.source.ITileDataSource; +import org.oscim.tiling.source.TileSource; import org.oscim.tiling.source.common.LwHttp; import org.oscim.tiling.source.common.PbfTileDataSource; import org.oscim.tiling.source.common.UrlTileSource; @@ -26,13 +27,13 @@ public class MapnikVectorTileSource extends UrlTileSource { @Override public ITileDataSource getDataSource() { - return new TileDataSource(mUrl); + return new TileDataSource(this, mUrl); } static class TileDataSource extends PbfTileDataSource { - public TileDataSource(URL url) { - super(new TileDecoder()); + public TileDataSource(TileSource tileSource, URL url) { + super(new TileDecoder(), tileSource.tileCache); mConn = new LwHttp(url, "image/png", "vector.pbf", true) { @Override diff --git a/vtm/src/org/oscim/tiling/source/oscimap/OSciMap1TileSource.java b/vtm/src/org/oscim/tiling/source/oscimap/OSciMap1TileSource.java index d51573fc..ad46a744 100644 --- a/vtm/src/org/oscim/tiling/source/oscimap/OSciMap1TileSource.java +++ b/vtm/src/org/oscim/tiling/source/oscimap/OSciMap1TileSource.java @@ -17,6 +17,7 @@ package org.oscim.tiling.source.oscimap; import java.net.URL; import org.oscim.tiling.source.ITileDataSource; +import org.oscim.tiling.source.TileSource; import org.oscim.tiling.source.common.LwHttp; import org.oscim.tiling.source.common.PbfTileDataSource; import org.oscim.tiling.source.common.UrlTileSource; @@ -29,12 +30,12 @@ public class OSciMap1TileSource extends UrlTileSource { @Override public ITileDataSource getDataSource() { - return new TileDataSource(mUrl); + return new TileDataSource(this, mUrl); } class TileDataSource extends PbfTileDataSource { - public TileDataSource(URL url) { - super(new TileDecoder()); + public TileDataSource(TileSource tileSource, URL url) { + super(new TileDecoder(), tileSource.tileCache); mConn = new LwHttp(url, "application/osmtile", "osmtile", false); } } diff --git a/vtm/src/org/oscim/tiling/source/oscimap2/OSciMap2TileSource.java b/vtm/src/org/oscim/tiling/source/oscimap2/OSciMap2TileSource.java index 37094fa2..e3dbd245 100644 --- a/vtm/src/org/oscim/tiling/source/oscimap2/OSciMap2TileSource.java +++ b/vtm/src/org/oscim/tiling/source/oscimap2/OSciMap2TileSource.java @@ -26,6 +26,7 @@ import org.oscim.core.TagSet; import org.oscim.core.Tile; import org.oscim.tiling.source.ITileDataSink; import org.oscim.tiling.source.ITileDataSource; +import org.oscim.tiling.source.TileSource; import org.oscim.tiling.source.common.LwHttp; import org.oscim.tiling.source.common.PbfDecoder; import org.oscim.tiling.source.common.PbfTileDataSource; @@ -37,12 +38,12 @@ public class OSciMap2TileSource extends UrlTileSource { @Override public ITileDataSource getDataSource() { - return new TileDataSource(mUrl); + return new TileDataSource(this, mUrl); } class TileDataSource extends PbfTileDataSource { - public TileDataSource(URL url) { - super(new TileDecoder()); + public TileDataSource(TileSource tileSource, URL url) { + super(new TileDecoder(), tileSource.tileCache); mConn = new LwHttp(url, "application/osmtile", "osmtile", false); } } diff --git a/vtm/src/org/oscim/tiling/source/oscimap4/OSciMap4TileSource.java b/vtm/src/org/oscim/tiling/source/oscimap4/OSciMap4TileSource.java index 3d9db73e..2757d552 100644 --- a/vtm/src/org/oscim/tiling/source/oscimap4/OSciMap4TileSource.java +++ b/vtm/src/org/oscim/tiling/source/oscimap4/OSciMap4TileSource.java @@ -17,6 +17,7 @@ package org.oscim.tiling.source.oscimap4; import java.net.URL; import org.oscim.tiling.source.ITileDataSource; +import org.oscim.tiling.source.TileSource; import org.oscim.tiling.source.common.LwHttp; import org.oscim.tiling.source.common.PbfTileDataSource; import org.oscim.tiling.source.common.UrlTileSource; @@ -25,12 +26,12 @@ public class OSciMap4TileSource extends UrlTileSource { @Override public ITileDataSource getDataSource() { - return new TileDataSource(mUrl); + return new TileDataSource(this, mUrl); } class TileDataSource extends PbfTileDataSource { - public TileDataSource(URL url) { - super(new TileDecoder()); + public TileDataSource(TileSource tileSource, URL url) { + super(new TileDecoder(), tileSource.tileCache); //mConn = new LwHttp(url, "application/x-protobuf", "vtm", false); mConn = new LwHttp(url, "image/png", "vtm", false); }