/*******************************************************************************
* Copyright (c) MOBAC developers
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package mobac.program.atlascreators;
import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Locale;
import mobac.exceptions.AtlasTestException;
import mobac.exceptions.MapCreationException;
import mobac.program.annotations.AtlasCreatorName;
import mobac.program.annotations.SupportedParameters;
import mobac.program.atlascreators.tileprovider.ConvertedRawTileProvider;
import mobac.program.interfaces.AtlasInterface;
import mobac.program.interfaces.MapSource;
import mobac.program.interfaces.MapSpace;
import mobac.program.interfaces.MapSpace.ProjectionCategory;
import mobac.program.interfaces.RequiresSQLite;
import mobac.program.model.Settings;
import mobac.program.model.TileImageParameters;
import mobac.program.model.TileImageParameters.Name;
import mobac.utilities.Utilities;
import mobac.utilities.jdbc.SQLiteLoader;
/**
* Atlas/Map creator for "BigPlanet-Maps application for Android" (offline SQLite maps)
* http://code.google.com/p/bigplanet/
* <p>
* Some source parts are taken from the "android-map.blogspot.com Version of Mobile Atlas Creator":
* http://code.google.com/p/android-map/
* </p>
* <p>
* Additionally the created BigPlanet SQLite database has one additional table containing special info needed by the
* Android application <a href="http://robertdeveloper.blogspot.com/search/label/rmaps.release" >RMaps</a>.<br>
* (Database statements: {@link #RMAPS_TABLE_INFO_DDL} and {@link #RMAPS_UPDATE_INFO_SQL} ).<br>
* Changes made by <a href="mailto:robertk506@gmail.com">Robert</a>, author of RMaps.
* <p>
*/
@AtlasCreatorName(value = "RMaps SQLite", type = "RMaps")
@SupportedParameters(names = { Name.format })
public class RMapsSQLite extends AtlasCreator implements RequiresSQLite {
private static final int MAX_BATCH_SIZE = 1000;
private static final String TABLE_DDL = "CREATE TABLE IF NOT EXISTS tiles (x int, y int, z int, s int, image blob, PRIMARY KEY (x,y,z,s))";
private static final String INDEX_DDL = "CREATE INDEX IF NOT EXISTS IND on tiles (x,y,z,s)";
private static final String INSERT_SQL = "INSERT or REPLACE INTO tiles (x,y,z,s,image) VALUES (?,?,?,0,?)";
private static final String RMAPS_TABLE_INFO_DDL = "CREATE TABLE IF NOT EXISTS info AS SELECT 99 AS minzoom, 0 AS maxzoom";
private static final String RMAPS_CLEAR_INFO_SQL = "DELETE FROM info;";
private static final String RMAPS_UPDATE_INFO_MINMAX_SQL = "INSERT INTO info (minzoom,maxzoom) VALUES (?,?);";
private static final String RMAPS_INFO_MAX_SQL = "SELECT DISTINCT z FROM tiles ORDER BY z DESC LIMIT 1;";
private static final String RMAPS_INFO_MIN_SQL = "SELECT DISTINCT z FROM tiles ORDER BY z ASC LIMIT 1;";
protected File databaseFile;
protected Connection conn = null;
protected PreparedStatement prepStmt;
public RMapsSQLite() {
super();
SQLiteLoader.loadSQLiteOrShowError();
}
@Override
public boolean testMapSource(MapSource mapSource) {
MapSpace mapSpace = mapSource.getMapSpace();
boolean correctTileSize = (256 == mapSpace.getTileSize());
ProjectionCategory pc = mapSpace.getProjectionCategory();
boolean correctProjection = (ProjectionCategory.SPHERE.equals(pc) || ProjectionCategory.ELLIPSOID.equals(pc));
return correctTileSize && correctProjection;
}
@Override
public void startAtlasCreation(AtlasInterface atlas, File customAtlasDir) throws IOException, AtlasTestException,
InterruptedException {
if (customAtlasDir == null)
customAtlasDir = Settings.getInstance().getAtlasOutputDirectory();
super.startAtlasCreation(atlas, customAtlasDir);
databaseFile = new File(atlasDir, getDatabaseFileName());
log.debug("SQLite Database file: " + databaseFile);
}
@Override
public void createMap() throws MapCreationException, InterruptedException {
try {
Utilities.mkDir(atlasDir);
} catch (IOException e) {
throw new MapCreationException(map, e);
}
try {
SQLiteLoader.loadSQLite();
} catch (SQLException e) {
throw new MapCreationException(SQLiteLoader.getMsgSqliteMissing(), map, e);
}
try {
openConnection();
initializeDB();
createTiles();
} catch (SQLException e) {
throw new MapCreationException("Error creating SQL database \"" + databaseFile + "\": " + e.getMessage(),
map, e);
}
}
protected void openConnection() throws SQLException {
if (conn == null || conn.isClosed()) {
String url = "jdbc:sqlite:/" + databaseFile.getAbsolutePath();
conn = DriverManager.getConnection(url);
}
}
@Override
public void abortAtlasCreation() throws IOException {
SQLiteLoader.closeConnection(conn);
conn = null;
super.abortAtlasCreation();
}
@Override
public void finishAtlasCreation() throws IOException, InterruptedException {
SQLiteLoader.closeConnection(conn);
conn = null;
super.finishAtlasCreation();
}
protected void initializeDB() throws SQLException {
Statement stat = conn.createStatement();
stat.executeUpdate(TABLE_DDL);
stat.executeUpdate(INDEX_DDL);
createInfoTable(stat);
stat.executeUpdate("CREATE TABLE IF NOT EXISTS android_metadata (locale TEXT)");
if (!(stat.executeQuery("SELECT * FROM android_metadata").next())) {
String locale = Locale.getDefault().toString();
stat.executeUpdate("INSERT INTO android_metadata VALUES ('" + locale + "')");
}
stat.close();
}
protected void createInfoTable(Statement stat) throws SQLException {
stat.executeUpdate(RMAPS_TABLE_INFO_DDL);
}
protected void createTiles() throws InterruptedException, MapCreationException {
int maxMapProgress = 2 * (xMax - xMin + 1) * (yMax - yMin + 1);
atlasProgress.initMapCreation(maxMapProgress);
TileImageParameters param = map.getParameters();
if (param != null)
mapDlTileProvider = new ConvertedRawTileProvider(mapDlTileProvider, param.getFormat());
try {
conn.setAutoCommit(false);
int batchTileCount = 0;
int tilesWritten = 0;
Runtime r = Runtime.getRuntime();
long heapMaxSize = r.maxMemory();
prepStmt = conn.prepareStatement(getTileInsertSQL());
for (int x = xMin; x <= xMax; x++) {
for (int y = yMin; y <= yMax; y++) {
checkUserAbort();
atlasProgress.incMapCreationProgress();
try {
byte[] sourceTileData = mapDlTileProvider.getTileData(x, y);
if (sourceTileData != null) {
writeTile(x, y, zoom, sourceTileData);
tilesWritten++;
long heapAvailable = heapMaxSize - r.totalMemory() + r.freeMemory();
batchTileCount++;
if ((heapAvailable < HEAP_MIN) || (batchTileCount >= MAX_BATCH_SIZE)) {
log.trace("Executing batch containing " + batchTileCount + " tiles");
prepStmt.executeBatch();
prepStmt.clearBatch();
System.gc();
conn.commit();
atlasProgress.incMapCreationProgress(batchTileCount);
batchTileCount = 0;
}
}
} catch (IOException e) {
throw new MapCreationException(map, e);
}
}
}
prepStmt.executeBatch();
prepStmt.clearBatch();
System.gc();
if (tilesWritten > 0)
updateTileMetaInfo();
log.trace("Final commit containing " + batchTileCount + " tiles");
conn.commit();
atlasProgress.setMapCreationProgress(maxMapProgress);
} catch (SQLException e) {
throw new MapCreationException(map, e);
}
}
protected void updateTileMetaInfo() throws SQLException {
Statement stat = conn.createStatement();
ResultSet rs = stat.executeQuery(RMAPS_INFO_MAX_SQL);
if (!rs.next())
throw new SQLException("failed to retrieve max tile zoom info");
int max = rs.getInt(1);
rs.close();
rs = stat.executeQuery(RMAPS_INFO_MIN_SQL);
if (!rs.next())
throw new SQLException("failed to retrieve min tile zoom info");
int min = rs.getInt(1);
rs.close();
PreparedStatement ps = conn.prepareStatement(RMAPS_UPDATE_INFO_MINMAX_SQL);
ps.setInt(1, min);
ps.setInt(2, max);
stat.execute(RMAPS_CLEAR_INFO_SQL);
ps.execute();
stat.close();
ps.close();
}
protected void writeTile(int x, int y, int z, byte[] tileData) throws SQLException, IOException {
prepStmt.setInt(1, x);
prepStmt.setInt(2, y);
prepStmt.setInt(3, 17 - z);
prepStmt.setBytes(4, tileData);
prepStmt.addBatch();
}
protected String getDatabaseFileName() {
return atlas.getName() + ".sqlitedb";
}
protected String getTileInsertSQL() {
return INSERT_SQL;
}
}