package net.osmand.plus;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.List;
import net.osmand.IndexConstants;
import net.osmand.PlatformUtil;
import net.osmand.data.QuadRect;
import net.osmand.map.ITileSource;
import net.osmand.map.TileSourceManager;
import net.osmand.map.TileSourceManager.TileSourceTemplate;
import net.osmand.plus.api.SQLiteAPI.SQLiteConnection;
import net.osmand.plus.api.SQLiteAPI.SQLiteCursor;
import net.osmand.util.Algorithms;
import org.apache.commons.logging.Log;
import bsh.Interpreter;
import android.database.sqlite.SQLiteDiskIOException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.widget.Toast;
public class SQLiteTileSource implements ITileSource {
public static final String EXT = IndexConstants.SQLITE_EXT;
private static final Log LOG = PlatformUtil.getLog(SQLiteTileSource.class);
private ITileSource base;
private String urlTemplate = null;
private String name;
private SQLiteConnection db = null;
private final File file;
private int minZoom = 1;
private int maxZoom = 17;
private boolean inversiveZoom = true; // BigPlanet
private boolean timeSupported = false;
private int expirationTimeMillis = -1; // never
private boolean isEllipsoid = false;
private String rule = null;
private String referer = null;
static final int tileSize = 256;
private OsmandApplication ctx;
private boolean onlyReadonlyAvailable = false;
public SQLiteTileSource(OsmandApplication ctx, File f, List<TileSourceTemplate> toFindUrl){
this.ctx = ctx;
this.file = f;
if (f != null) {
int i = f.getName().lastIndexOf('.');
name = f.getName().substring(0, i);
i = name.lastIndexOf('.');
if (i > 0) {
String sourceName = name.substring(i + 1);
for (TileSourceTemplate is : toFindUrl) {
if (is.getName().equalsIgnoreCase(sourceName)) {
base = is;
urlTemplate = is.getUrlTemplate();
break;
}
}
}
}
}
@Override
public int getBitDensity() {
return base != null ? base.getBitDensity() : 16;
}
@Override
public int getMaximumZoomSupported() {
return base != null ? base.getMaximumZoomSupported() : maxZoom;
}
@Override
public int getMinimumZoomSupported() {
return base != null ? base.getMinimumZoomSupported() : minZoom;
}
@Override
public String getName() {
return name;
}
@Override
public String getTileFormat() {
return base != null ? base.getTileFormat() : ".png"; //$NON-NLS-1$
}
@Override
public int getTileSize() {
return base != null ? base.getTileSize() : tileSize;
}
Interpreter bshInterpreter = null;
@Override
public String getUrlToLoad(int x, int y, int zoom) {
if (zoom > maxZoom)
return null;
SQLiteConnection db = getDatabase();
if(db == null || db.isReadOnly() || urlTemplate == null){
return null;
}
if(TileSourceManager.RULE_BEANSHELL.equalsIgnoreCase(rule)){
try {
if(bshInterpreter == null){
bshInterpreter = new Interpreter();
bshInterpreter.eval(urlTemplate);
}
return (String) bshInterpreter.eval("getTileUrl("+zoom+","+x+","+y+");");
} catch (bsh.EvalError e) {
LOG.debug("getUrlToLoad Error" + e.getMessage());
Toast.makeText(ctx, e.getMessage(), Toast.LENGTH_LONG).show();
LOG.error(e.getMessage(), e);
return null;
}
}
else {
return MessageFormat.format(urlTemplate, zoom+"", x+"", y+""); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((base == null) ? 0 : base.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
SQLiteTileSource other = (SQLiteTileSource) obj;
if (base == null) {
if (other.base != null)
return false;
} else if (!base.equals(other.base))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
protected SQLiteConnection getDatabase(){
if((db == null || db.isClosed()) && file.exists() ){
LOG.debug("Open " + file.getAbsolutePath());
try {
onlyReadonlyAvailable = false;
db = ctx.getSQLiteAPI().openByAbsolutePath(file.getAbsolutePath(), false);
} catch(RuntimeException e) {
onlyReadonlyAvailable = true;
db = ctx.getSQLiteAPI().openByAbsolutePath(file.getAbsolutePath(), true);
}
try {
SQLiteCursor cursor = db.rawQuery("SELECT * FROM info", null);
if(cursor.moveToFirst()) {
String[] columnNames = cursor.getColumnNames();
List<String> list = Arrays.asList(columnNames);
int url = list.indexOf("url");
if(url != -1) {
String template = cursor.getString(url);
if(!Algorithms.isEmpty(template)){
//urlTemplate = template;
urlTemplate = TileSourceTemplate.normalizeUrl(template);
}
}
int ruleId = list.indexOf("rule");
if(ruleId != -1) {
rule = cursor.getString(ruleId);
}
int refererId = list.indexOf("referer");
if(refererId != -1) {
referer = cursor.getString(refererId);
}
int tnumbering = list.indexOf("tilenumbering");
if(tnumbering != -1) {
inversiveZoom = "BigPlanet".equalsIgnoreCase(cursor.getString(tnumbering));
} else {
inversiveZoom = true;
addInfoColumn("tilenumbering", "BigPlanet");
}
int timecolumn = list.indexOf("timecolumn");
if (timecolumn != -1) {
timeSupported = "yes".equalsIgnoreCase(cursor.getString(timecolumn));
} else {
timeSupported = hasTimeColumn();
addInfoColumn("timecolumn", timeSupported?"yes" : "no");
}
int expireminutes = list.indexOf("expireminutes");
this.expirationTimeMillis = -1;
if(expireminutes != -1) {
int minutes = (int) cursor.getInt(expireminutes);
if(minutes > 0) {
this.expirationTimeMillis = minutes * 60 * 1000;
}
} else {
addInfoColumn("expireminutes", "0");
}
int ellipsoid = list.indexOf("ellipsoid");
if(ellipsoid != -1) {
int set = (int) cursor.getInt(ellipsoid);
if(set == 1){
this.isEllipsoid = true;
}
}
//boolean inversiveInfoZoom = tnumbering != -1 && "BigPlanet".equals(cursor.getString(tnumbering));
boolean inversiveInfoZoom = inversiveZoom;
int mnz = list.indexOf("minzoom");
if(mnz != -1) {
minZoom = (int) cursor.getInt(mnz);
}
int mxz = list.indexOf("maxzoom");
if(mxz != -1) {
maxZoom = (int) cursor.getInt(mxz);
}
if(inversiveInfoZoom) {
mnz = minZoom;
minZoom = 17 - maxZoom;
maxZoom = 17 - mnz;
}
}
cursor.close();
} catch (RuntimeException e) {
e.printStackTrace();
}
}
return db;
}
private void addInfoColumn(String columnName, String value) {
if(!onlyReadonlyAvailable) {
db.execSQL("alter table info add column "+columnName+" TEXT");
db.execSQL("update info set "+columnName+" = '"+value+"'");
}
}
private boolean hasTimeColumn() {
SQLiteCursor cursor;
cursor = db.rawQuery("SELECT * FROM tiles", null);
cursor.moveToFirst();
List<String> cols = Arrays.asList(cursor.getColumnNames());
boolean timeSupported = cols.contains("time");
cursor.close();
return timeSupported;
}
public boolean exists(int x, int y, int zoom) {
SQLiteConnection db = getDatabase();
if (db == null) {
return false;
}
try {
int z = getFileZoom(zoom);
SQLiteCursor cursor = db.rawQuery(
"SELECT 1 FROM tiles WHERE x = ? AND y = ? AND z = ?", new String[] { x + "", y + "", z + "" }); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$//$NON-NLS-4$
try {
boolean e = cursor.moveToFirst();
cursor.close();
return e;
} catch (SQLiteDiskIOException e) {
return false;
}
} finally {
if (LOG.isDebugEnabled()) {
long time = System.currentTimeMillis();
LOG.debug("Checking tile existance x = " + x + " y = " + y + " z = " + zoom + " for " + (System.currentTimeMillis() - time)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
}
}
}
public boolean isLocked() {
SQLiteConnection db = getDatabase();
if(db == null){
return false;
}
return db.isDbLockedByOtherThreads();
}
public byte[] getBytes(int x, int y, int zoom, String dirWithTiles, long[] timeHolder) throws IOException {
SQLiteConnection db = getDatabase();
if(db == null){
return null;
}
long ts = System.currentTimeMillis();
try {
if (zoom <= maxZoom) {
// return the normal tile if exists
String[] params = new String[] { x + "", y + "", getFileZoom(zoom) + "" };
boolean queryTime = timeHolder != null && timeHolder.length > 0 && timeSupported;
SQLiteCursor cursor = db.rawQuery("SELECT image " +(queryTime?", time":"")+" FROM tiles WHERE x = ? AND y = ? AND z = ?",
params); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$//$NON-NLS-4$
byte[] blob = null;
if (cursor.moveToFirst()) {
blob = cursor.getBlob(0);
if(queryTime) {
timeHolder[0] = cursor.getLong(1);
}
}
cursor.close();
return blob;
}
return null;
} finally {
if(LOG.isDebugEnabled()) {
LOG.debug("Load tile " + x + "/" + y + "/" + zoom + " for " + (System.currentTimeMillis() - ts)
+ " ms ");
}
}
}
@Override
public byte[] getBytes(int x, int y, int zoom, String dirWithTiles) throws IOException {
return getBytes(x, y, zoom, dirWithTiles, null);
}
public Bitmap getImage(int x, int y, int zoom, long[] timeHolder) {
SQLiteConnection db = getDatabase();
if(db == null){
return null;
}
String[] params = new String[] { x + "", y + "", getFileZoom(zoom) + "" };
byte[] blob;
try {
blob = getBytes(x, y, zoom, null, timeHolder);
} catch (IOException e) {
return null;
}
if (blob != null) {
Bitmap bmp = null;
bmp = BitmapFactory.decodeByteArray(blob, 0, blob.length);
if(bmp == null) {
// broken image delete it
db.execSQL("DELETE FROM tiles WHERE x = ? AND y = ? AND z = ?", params);
}
return bmp;
}
return null;
}
public ITileSource getBase() {
return base;
}
public QuadRect getRectBoundary(int coordinatesZoom, int minZ){
SQLiteConnection db = getDatabase();
if(db == null || coordinatesZoom > 25 ){
return null;
}
SQLiteCursor q ;
if (inversiveZoom) {
int minZoom = (17 - minZ) + 1;
// 17 - z = zoom, x << (25 - zoom) = 25th x tile = 8 + z,
q = db.rawQuery("SELECT max(x << (8+z)), min(x << (8+z)), max(y << (8+z)), min(y << (8+z))" +
" from tiles where z < "
+ minZoom, new String[0]);
} else {
q = db.rawQuery("SELECT max(x << (25-z)), min(x << (25-z)), max(y << (25-z)), min(y << (25-z))"
+ " from tiles where z > " + minZ,
new String[0]);
}
q.moveToFirst();
int right = (int) (q.getInt(0) >> (25 - coordinatesZoom));
int left = (int) (q.getInt(1) >> (25 - coordinatesZoom));
int top = (int) (q.getInt(3) >> (25 - coordinatesZoom));
int bottom = (int) (q.getInt(2) >> (25 - coordinatesZoom));
return new QuadRect(left, top, right, bottom);
}
public void deleteImage(int x, int y, int zoom) {
SQLiteConnection db = getDatabase();
if(db == null || db.isReadOnly()){
return;
}
db.execSQL("DELETE FROM tiles WHERE x = ? AND y = ? AND z = ?", new String[] {x+"", y+"", getFileZoom(zoom)+""}); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$//$NON-NLS-4$
}
private static final int BUF_SIZE = 1024;
public void insertImage(int x, int y, int zoom, File fileToSave) throws IOException {
ByteBuffer buf = ByteBuffer.allocate((int) fileToSave.length());
FileInputStream is = new FileInputStream(fileToSave);
int i = 0;
byte[] b = new byte[BUF_SIZE];
while ((i = is.read(b, 0, BUF_SIZE)) > -1) {
buf.put(b, 0, i);
}
insertImage(x, y, zoom, buf.array());
is.close();
}
@Override
public void clearTiles(String path) {
SQLiteConnection db = getDatabase();
if (db == null || db.isReadOnly() || onlyReadonlyAvailable) {
return;
}
db.execSQL("TRUNCATE TABLE tiles");
}
/**
* Makes method synchronized to give a little more time for get methods and
* let all writing attempts to wait outside of this method
*/
public /*synchronized*/ void insertImage(int x, int y, int zoom, byte[] dataToSave) throws IOException {
SQLiteConnection db = getDatabase();
if (db == null || db.isReadOnly() || onlyReadonlyAvailable) {
return;
}
/*There is no sense to downoad and do not save. If needed, check should perform before downlad
if (exists(x, y, zoom)) {
return;
}*/
String query = timeSupported ? "INSERT OR REPLACE INTO tiles(x,y,z,s,image,time) VALUES(?, ?, ?, ?, ?, ?)"
: "INSERT OR REPLACE INTO tiles(x,y,z,s,image) VALUES(?, ?, ?, ?, ?)";
net.osmand.plus.api.SQLiteAPI.SQLiteStatement statement = db.compileStatement(query); //$NON-NLS-1$
statement.bindLong(1, x);
statement.bindLong(2, y);
statement.bindLong(3, getFileZoom(zoom));
statement.bindLong(4, 0);
statement.bindBlob(5, dataToSave);
if (timeSupported) {
statement.bindLong(6, System.currentTimeMillis());
}
statement.execute();
statement.close();
}
private int getFileZoom(int zoom) {
return inversiveZoom ? 17 - zoom : zoom;
}
public void closeDB(){
LOG.debug("closeDB");
bshInterpreter = null;
if(timeSupported)
clearOld();
if(db != null){
db.close();
db = null;
}
}
public void clearOld() {
SQLiteConnection db = getDatabase();
if(db == null || db.isReadOnly()){
return;
}
LOG.debug("DELETE FROM tiles WHERE time<" + (System.currentTimeMillis() - getExpirationTimeMillis()));
db.execSQL("DELETE FROM tiles WHERE time<"+(System.currentTimeMillis()-getExpirationTimeMillis())); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$//$NON-NLS-4$
db.execSQL("VACUUM"); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$//$NON-NLS-4$
}
@Override
public boolean couldBeDownloadedFromInternet() {
if(getDatabase() == null || getDatabase().isReadOnly() || onlyReadonlyAvailable){
return false;
}
return urlTemplate != null;
}
@Override
public boolean isEllipticYTile() {
return this.isEllipsoid;
//return false;
}
public int getExpirationTimeMinutes() {
if(expirationTimeMillis < 0) {
return -1;
}
return expirationTimeMillis / (60 * 1000);
}
public int getExpirationTimeMillis() {
return expirationTimeMillis;
}
public String getReferer() {
return referer;
}
}