/*
* Geopaparazzi - Digital field mapping on Android based devices
* Copyright (C) 2010 HydroloGIS (www.hydrologis.com)
*
* 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 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 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 eu.geopaparazzi.spatialite.database.spatial.core.databasehandlers;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import eu.geopaparazzi.library.database.GPLog;
import eu.geopaparazzi.spatialite.database.spatial.core.tables.AbstractSpatialTable;
import eu.geopaparazzi.spatialite.database.spatial.core.tables.SpatialRasterTable;
import eu.geopaparazzi.spatialite.database.spatial.core.tables.SpatialVectorTable;
import eu.geopaparazzi.spatialite.database.spatial.core.mbtiles.MBTilesDroidSpitter;
import eu.geopaparazzi.spatialite.database.spatial.core.mbtiles.MBtilesAsync;
import eu.geopaparazzi.spatialite.database.spatial.core.mbtiles.MbTilesMetadata;
import eu.geopaparazzi.library.util.types.ESpatialDataSources;
import jsqlite.Exception;
/**
* An utility class to handle an mbtiles database.
* <p/>
* author Andrea Antonello (www.hydrologis.com)
* adapted to create and fill mbtiles databases Mark Johnson (www.mj10777.de)
*/
@SuppressWarnings("nls")
public class MbtilesDatabaseHandler extends AbstractSpatialDatabaseHandler {
private List<SpatialRasterTable> rasterTableList;
private MBTilesDroidSpitter mbtilesSplitter;
private HashMap<String, String> mbtilesMetadata = null;
/**
*
*/
public HashMap<String, String> async_mbtiles_metadata = null;
private MBtilesAsync mbtiles_async = null;
private boolean isOpen;
@SuppressWarnings("javadoc")
public static enum AsyncTasks {
ASYNC_PARMS, //
ANALYZE_VACUUM, //
REQUEST_URL, //
REQUEST_CREATE, //
REQUEST_DROP, //
REQUEST_DELETE, //
REQUEST_PING, //
UPDATE_BOUNDS, //
RESET_METADATA
}
/**
* List of async tasks to be completed.
*/
public List<MbtilesDatabaseHandler.AsyncTasks> asyncTasksList = new ArrayList<MbtilesDatabaseHandler.AsyncTasks>();
/** * */
public String s_request_url_source = "";
/** * */
public String s_request_protocol = ""; // 'file' or 'http'
/** * */
public String s_request_bounds = "";
/** * */
public String s_request_bounds_url = "";
/** * */
public String s_request_zoom_levels = "";
/** * */
public String s_request_zoom_levels_url = "";
/** * */
public String s_request_y_type = "osm"; // 0=osm ; 1=tms ; 2=wms
/** * */
public String s_request_type = ""; // 'fill', 'replace'
/**
* Constructor.
* <p/>
* <ul>
* <li>if the file does not exist, a valid mbtile database will be created</li>
* <li>if the parent directory does not exist, it will be created</li>
* </ul>
*
* @param dbPath full path to mbtiles file to open.
* @param initMetadata list of initial metadata values to set
* upon creation (can be <code>null</code>).
* @throws IOException if something goes wrong.
*/
public MbtilesDatabaseHandler(String dbPath, HashMap<String, String> initMetadata) throws IOException {
super(dbPathCheck(dbPath));
this.mbtilesMetadata = initMetadata;
mbtilesSplitter = new MBTilesDroidSpitter(databaseFile, mbtilesMetadata);
}
/**
* @param dbPath
* @return
* @mj107777 WHY IS THIS DONE HERE? DOES THIS WORK?
*/
private static String dbPathCheck(String dbPath) {
if (!dbPath.endsWith(ESpatialDataSources.MBTILES.getExtension())) {
// .mbtiles files must have an .mbtiles
// extension, force this
dbPath = dbPath.substring(0, dbPath.lastIndexOf(".")) + ESpatialDataSources.MBTILES.getExtension();
}
return dbPath;
}
public boolean isValid() {
if (mbtilesSplitter.getmbtiles() == null) { // in case .'open' was forgotten
open(); // "" : default value will be used '1.1'
}
return mbtilesSplitter.isValid();
}
/**
* Called during Construction of Async-Tasks.
* <p/>
* <p>- Database connection needed
*
* @return list of Tasks to be completed.
*/
public List<MbtilesDatabaseHandler.AsyncTasks> getAsyncTasks() {
if (mbtilesSplitter.getmbtiles() == null) { // in case .'open' was forgotten
open(); // "" : default value will be used '1.1'
}
return asyncTasksList;
}
public List<SpatialVectorTable> getSpatialVectorTables(boolean forceRead) throws Exception {
return Collections.emptyList();
}
public List<SpatialRasterTable> getSpatialRasterTables(boolean forceRead) throws Exception {
if (rasterTableList == null || forceRead) {
rasterTableList = new ArrayList<SpatialRasterTable>();
open();
double[] d_bounds = {this.boundsWest, this.boundsSouth, this.boundsEast, this.boundsNorth};
SpatialRasterTable table = new SpatialRasterTable(databasePath, databaseFileNameNoExtension, "3857", this.minZoom,
this.maxZoom, centerX, centerY, "?,?,?", d_bounds);
table.setDefaultZoom(defaultZoom);
// table.setDescription(getDescription());
table.setMapType(ESpatialDataSources.MBTILES.getTypeName());
rasterTableList.add(table);
}
return rasterTableList;
}
public float[] getTableBounds(AbstractSpatialTable spatialTable) throws Exception {
MbTilesMetadata metadata = mbtilesSplitter.getMetadata();
float[] bounds = metadata.bounds;// left, bottom, right, top
float w = bounds[0];
float s = bounds[1];
float e = bounds[2];
float n = bounds[3];
return new float[]{n, s, e, w};
}
public byte[] getRasterTile(String query) {
String[] split = query.split(",");
if (split.length != 3) {
return null;
}
int i_z = 0;
int i_x = 0;
int i_y_osm = 0;
try {
i_z = Integer.parseInt(split[0]);
i_x = Integer.parseInt(split[1]);
i_y_osm = Integer.parseInt(split[2]);
} catch (NumberFormatException e) {
return null;
}
byte[] tileAsBytes = mbtilesSplitter.getTileAsBytes(i_x, i_y_osm, i_z);
return tileAsBytes;
}
/**
* Function to retrieve Tile Bitmap from the mbtiles Database.
* <p/>
* <p>i_y_osm must be in is Open-Street-Map 'Slippy Map' notation
* [will be converted to 'tms' notation if needed]
*
* @param i_x the value for tile_column field in the map,tiles Tables and part of the tile_id when image is not blank
* @param i_y_osm the value for tile_row field in the map,tiles Tables and part of the tile_id when image is not blank
* @param i_z the value for zoom_level field in the map,tiles Tables and part of the tile_id when image is not blank
* @param i_pixel_size the value for zoom_level field in the map,tiles Tables and part of the tile_id when image is not blank
* @param tile_bitmap retrieve the Bitmap as done in 'CustomTileDownloader'
* @return Bitmap of the tile or null if no tile matched the given parameters
*/
public boolean getBitmapTile(int i_x, int i_y_osm, int i_z, int i_pixel_size, Bitmap tile_bitmap) {
boolean b_rc = true;
if (mbtilesSplitter.getmbtiles() == null) { // in case .'open' was forgotten
open(); // "" : default value will be used '1.1'
}
int[] pixels = new int[i_pixel_size * i_pixel_size];
byte[] rasterBytes = mbtilesSplitter.getTileAsBytes(i_x, i_y_osm, i_z);
if (rasterBytes == null) {
b_rc = false;
return b_rc;
}
Bitmap decodedBitmap = null;
decodedBitmap = BitmapFactory.decodeByteArray(rasterBytes, 0, rasterBytes.length);
// check if the input stream could be decoded into a bitmap
if (decodedBitmap != null) {
// copy all pixels from the decoded bitmap to the color array
decodedBitmap.getPixels(pixels, 0, i_pixel_size, 0, 0, i_pixel_size, i_pixel_size);
decodedBitmap.recycle();
} else {
b_rc = false;
return b_rc;
}
// copy all pixels from the color array to the tile bitmap
tile_bitmap.setPixels(pixels, 0, i_pixel_size, 0, 0, i_pixel_size, i_pixel_size);
return b_rc;
}
/**
* Function to insert a new Tile Bitmap to the mbtiles Database
* <p/>
* <ul>
* <li>i_y_osm must be in is Open-Street-Map 'Slippy Map' notation [will
* be converted to 'tms' notation if needed]</li>
* <li>checking will be done to determine if the Bitmap is blank [i.e.
* all pixels have the same RGB]</li>
* </ul>
*
* @param i_x the value for tile_column field in the map,tiles Tables and part of the tile_id when image is not blank
* @param i_y_osm the value for tile_row field in the map,tiles Tables and part of the tile_id when image is not blank
* @param i_z the value for zoom_level field in the map,tiles Tables and part of the tile_id when image is not blank
* @param tile_bitmap the Bitmap to extract image-data extracted from. [Will be converted to JPG or PNG depending on metdata setting]
* @param forceUnique if 1, it check if image is unique in Database [may be slow if used]
* @return 0: correct, otherwise error
* @throws IOException if something goes wrong.
*/
public int insertBitmapTile(int i_x, int i_y_osm, int i_z, Bitmap tile_bitmap, int forceUnique) throws IOException {
try {
return mbtilesSplitter.insertBitmapTile(i_x, i_y_osm, i_z, tile_bitmap, forceUnique);
} catch (IOException e) {
GPLog.error(this, null, e);
return 1;
}
}
public void open() {
if (mbtilesSplitter.getmbtiles() == null) {
mbtilesSplitter.open(true, ""); // "" : default value will be used '1.1'
loadMetadata();
isOpen = true;
}
}
@Override
public boolean isOpen() {
return isOpen;
}
/**
* Load and set metadata from mbtiles Database, with all default tasks
* <p/>
* <p>- do this in one place to insure that it is allways done in the same way.
*/
public void loadMetadata() {
MbTilesMetadata metadata = mbtilesSplitter.getMetadata();
float[] bounds = metadata.bounds;// left, bottom, right, top
double[] d_bounds = {bounds[0], bounds[1], bounds[2], bounds[3]};
float[] center = metadata.center;// center_x,center_y,zoom
this.databaseFileNameNoExtension = metadata.name;
// String tableName = metadata.name;
this.defaultZoom = metadata.maxZoom;
this.minZoom = metadata.minZoom;
this.maxZoom = metadata.maxZoom;
this.boundsWest = d_bounds[0];
this.boundsSouth = d_bounds[1];
this.boundsEast = d_bounds[2];
this.boundsNorth = d_bounds[3];
if (center != null) {
this.centerX = center[0];
this.centerY = center[1];
this.defaultZoom = (int) center[2];
} else {
if (bounds != null) {
this.centerX = bounds[0] + (bounds[2] - bounds[0]) / 2f;
this.centerY = bounds[1] + (bounds[3] - bounds[1]) / 2f;
}
}
// setDescription(metadata.description);
}
public void close() throws Exception {
isOpen = false;
if (mbtiles_async != null) {
if (mbtiles_async.getStatus() == AsyncTask.Status.RUNNING) {
mbtiles_async.cancel(true);
}
}
if (mbtilesSplitter != null) {
mbtilesSplitter.close();
}
}
/**
* Return list of all zoom-levels and Bounds in LatLong.
* <p/>
* <br>- last entry: min/max zoom-levels and Bounds
* <br>- this is calculated from the Database and will update the metadata-table
*
* @return map of zoom-levels and Bounds in LatLong
*/
public HashMap<String, String> getBoundsZoomLevels() {
if (mbtilesSplitter != null) {
return mbtilesSplitter.getBoundsZoomLevels();
}
return new LinkedHashMap<String, String>();
}
/**
* Return center position with zoom-level.
*
* @return Center as [lon, lat, default zoom]
*/
public String getCenterParms() {
if (mbtilesSplitter != null) {
return mbtilesSplitter.getCenterParms();
}
return "";
}
/**
* Update mbtiles Bounds / Zoom (min/max) levels
*
* @param doReloadMetadata if 1 reload values after update [not needed upon creation, update after bounds/center/zoom changes]
* @return o if reading was ok.
*/
public int updateBounds(int doReloadMetadata) {
if (mbtilesSplitter != null) {
mbtilesSplitter.fetch_bounds_minmax(doReloadMetadata, 1);
loadMetadata(); // will read and reset values
return 0;
}
return 1;
}
/**
* General Function to update mbtiles metadata Table.
*
* @param mbtilesMetadata list of key,values to update. [fill this with valued that need to be added/changed]
* @param doReloadMetadata 1: reload values after update [not needed upon creation, update after bounds/center/zoom changes]
* @return 0: no error
* @throws IOException if something goes wrong.
*/
public int updateMetadata(HashMap<String, String> mbtilesMetadata, int doReloadMetadata) throws IOException {
int i_rc = 1;
if (mbtilesSplitter != null) {
try {
i_rc = mbtilesSplitter.update_mbtiles_metadata(null, mbtilesMetadata, doReloadMetadata);
if (doReloadMetadata == 1)
loadMetadata(); // will read and reset values
i_rc = 0;
} catch (IOException e) {
GPLog.androidLog(4, "MbtilesDatabaseHandler.update_metadata[" + getDatabasePath() + "]", e);
}
}
return i_rc;
}
/**
* Launch async retrieve url.
*
* @param mbtiles_request_url a map of url retrival info.
* @param async_mbtiles_metadata a map of mbtiles metadata.
*/
public void runRetrieveUrl(HashMap<String, String> mbtiles_request_url, HashMap<String, String> async_mbtiles_metadata) {
int i_run_create = 0;
int i_run_fill = 0;
int i_run_replace = 0;
int i_load_url = 0;
int i_delete = 0;
int i_drop = 0;
int i_vacuum = 0;
int i_update_bounds = 0;
this.async_mbtiles_metadata = async_mbtiles_metadata;
for (Map.Entry<String, String> request_url : mbtiles_request_url.entrySet()) {
String s_key = request_url.getKey();
String s_value = request_url.getValue();
if (s_key.equals("request_type")) {
if (s_value.indexOf("fill") != -1) { // will request missing tiles only
i_run_fill = 1;
s_request_type = "fill";
}
if (s_value.indexOf("replace") != -1) { // will replace existing tiles
i_run_replace = 1;
s_request_type = "replace";
}
if (s_value.indexOf("load") != -1) { // will replace existing tiles
i_load_url = 1;
}
if (s_value.indexOf("drop") != -1) { // will delete the requested tiles, retaining
// the allready downloaded tiles
i_drop = 1;
}
if (s_value.indexOf("vacuum") != -1) { // will delete the requested tiles, retaining
// the allready downloaded tiles
i_vacuum = 1;
}
if (s_value.indexOf("update_bounds") != -1) { // will do an extensive check on
// bounds and zoom-level, updating the
// mbtiles.metadata table
i_update_bounds = 1;
}
if (s_value.indexOf("delete") != -1) { // planned for future
i_delete = 1;
}
}
if (s_key.equals("request_url")) {
s_request_url_source = s_value;
}
if (s_key.equals("request_bounds")) {
s_request_bounds = s_value;
}
if (s_key.equals("request_bounds_url")) {
s_request_bounds_url = s_value;
}
if (s_key.equals("request_zoom_levels")) {
s_request_zoom_levels = s_value;
}
if (s_key.equals("request_zoom_levels_url")) {
// reserved for future
s_request_zoom_levels_url = s_value;
}
if (s_key.equals("request_y_type")) {
// reserved for future
s_request_y_type = s_value;
}
if (s_key.equals("request_protocol")) {
// 'file' or 'http'
s_request_protocol = s_value;
}
// GPLog.androidLog(-1, "run_retrieve_url: key[" + s_key + "] value[" + s_value +
// "] load[" + i_load_url + "] ");
}
// check if the pre-requriment for REQUEST_CREATE are fullfilled
if ((i_run_fill != 0) || (i_run_replace != 1)) {
if ((i_run_fill == 1) && (i_run_replace == 1)) {
i_run_replace = 0;
s_request_type = "fill";
}
if ((!s_request_url_source.equals("")) && (!s_request_bounds.equals("")) && (!s_request_zoom_levels.equals(""))) {
// run only if set, some cheding might be wise
i_run_create = 1;
}
}
// The order of adding is important
if (i_update_bounds > 0) { // will do an extensive check on bounds and zoom-level, updating
// the mbtiles.metadata table
asyncTasksList.add(AsyncTasks.UPDATE_BOUNDS);
}
if (i_drop > 0) { // this should effectaly delete exiting request and reload again if
// requested
asyncTasksList.add(AsyncTasks.REQUEST_DROP);
}
if (i_delete > 0) { // planned for future [delete tiles of an area]
asyncTasksList.add(AsyncTasks.REQUEST_DELETE);
}
if (i_vacuum > 0) { // VACUUM should run AFTER any deleting and BEFORE any inserting
asyncTasksList.add(AsyncTasks.ANALYZE_VACUUM);
}
if (i_run_create > 0) { // REQUEST_CREATE
asyncTasksList.add(AsyncTasks.REQUEST_CREATE);
}
if (i_load_url > 0) { // will download requested tiles
asyncTasksList.add(AsyncTasks.REQUEST_URL);
}
if ((this.async_mbtiles_metadata != null) && (this.async_mbtiles_metadata.size() > 0)) {
asyncTasksList.add(AsyncTasks.RESET_METADATA);
}
if (asyncTasksList.size() > 0) {
mbtiles_async = new MBtilesAsync(this);
// with .execute(): this crashes
// mbtiles_async.execute(AsyncTasks.ASYNC_PARMS);
/*
* moovida: THIS IS NOT 2.3.3 compatible, which is 10 and < 12
* if it crashes, we need to fin out why, but we can't use
* mbtiles_async.executeOnExecutor.
*/
// if (Build.VERSION.SDK_INT < 12) // use numbers for backwards compatibility
// Build.VERSION_CODES.HONEYCOMB)
// { // http://developer.android.com/reference/android/os/Build.VERSION_CODES.html
// // GPLog.androidLog(-1,"run_retrieve_url.HONEYCOMB.["+Build.VERSION.SDK_INT+"]");
// mbtiles_async.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,AsyncTasks.ASYNC_PARMS);
// }
// else
// {
// GPLog.androidLog(-1,"run_retrieve_url.OTHER.["+Build.VERSION.SDK_INT+"]");
mbtiles_async.execute(AsyncTasks.ASYNC_PARMS);
// }
// GPLog.androidLog(-1,"run_retrieve_url.Build.VERSION.SDK_INT.["+Build.VERSION.SDK_INT+"]");
// // 20131125: 15, 2031221: 17
// mbtiles_async.execute(AsyncTasks.ASYNC_PARMS);
}
}
/**
* Returns list of collected urlmapped to their tile id.
*
* @param limit amount of records to retrieve [i_limit < 1 == all]
* @return the map of ids, urls.
*/
public HashMap<String, String> getRequestUrlsMap(int limit) {
if (mbtilesSplitter != null) {
return mbtilesSplitter.retrieve_request_url(limit);
}
return new LinkedHashMap<String, String>();
}
/**
* Bulk insert of record in table.
* <p/>
* - the request_url table will be created if it does not exist
*
* @param requestUrlsMap the map of urls to request.
* @return number of opened requests.
*/
public int bulkInsertFromUrlsTilesInTable(HashMap<String, String> requestUrlsMap) {
if (mbtilesSplitter != null) {
return mbtilesSplitter.insert_list_request_url(requestUrlsMap);
}
return -1;
}
/**
* Returns amount of records of table: request_url
* <p/>
* <p>parm values:
* <br>0: return existing value [set when database was opended,not reading the table] [MBTilesDroidSpitter.i_request_url_read_value]
* <br>1 : return existing value return existing value [reading the table with count after checking if it exits] [MBTilesDroidSpitter.i_request_url_read_db]
* <br>2: create table (if it does not exist) [MBTilesDroidSpitter.i_request_url_count_create]
* <br>3: delete table (if it does exist) [MBTilesDroidSpitter.i_request_url_count_drop]
*
* @param parm type of result
* @return if < 0: table does not exist; 0=exist but is empty ; > 0 open requests
*/
public int getRequestUrlCount(int parm) {
if (mbtilesSplitter != null) {
return mbtilesSplitter.get_request_url_count(parm);
}
return -1;
}
/**
* Delete of record in table: request_url
* <p/>
* <p>parm values: [3 only for internal use] - only 4 supported
* <br>3: insert record with: s_tile_id and s_tile_url [MBTilesDroidSpitter.i_request_url_count_insert]
* <br>4: delete record with: s_tile_id, delete table if count is 0 [MBTilesDroidSpitter.i_request_url_count_delete]
*
* @param s_tile_id tile_id to use
* @return if< 0: table does not exist; 0=exist but is empty ; > 0 open requests
*/
public int deleteRequestUrl(String s_tile_id) {
if (mbtilesSplitter != null) {
return mbtilesSplitter.insert_request_url(MBTilesDroidSpitter.i_request_url_count_delete, s_tile_id, "");
}
return -1;
}
/**
* Retrieves a list of tile id requested, based on bounds and zoom-level.
* <p/>
* <p>return values values:
* <br>tile_id : created with: get_tile_id_from_zxy
* <br>- will be read and parsed with: get_zxy_from_tile_id in on_request_create_url
* <br>s_request_type: 'fill': only missing tiles ; 'replace' all tiles ; 'exists' tiles that exist
*
* @param request_bounds bounds of request area
* @param i_zoom_level zoom level of tiles
* @param s_request_type request type ['fill','replace','exists']
* @param s_url_source TODO
* @param s_request_y_type TODO
* @return list of 'tile_id' needed
*/
public List<String> buildRequestList(double[] request_bounds, int i_zoom_level, String s_request_type, String s_url_source,
String s_request_y_type) {
if (mbtilesSplitter != null) {
return mbtilesSplitter.build_request_list(request_bounds, i_zoom_level, s_request_type, s_url_source,
s_request_y_type);
}
return new ArrayList<String>();
}
/**
* House-keeping tasks for Database.
* <p/>
* <p>The ANALYZE command gathers statistics fragment_about tables and indices
* <br>The VACUUM command rebuilds the entire database.
* <br>- A VACUUM will fail if there is an open transaction, or if there are one or more active SQL statements when it is run.
*
* @return 0=correct ; 1=ANALYSE has failed ; 2=VACUUM has failed
*/
public int on_analyze_vacuum() {
int i_rc = 0;
if (mbtilesSplitter != null) {
return mbtilesSplitter.on_analyze_vacuum();
}
return i_rc;
}
}