/** * @author Simon Thépot aka djcoin <simon.thepot@gmail.com, simon.thepot@makina-corpus.com> * adapted to create and fill mbtiles databases Mark Johnson (www.mj10777.de) */ package eu.geopaparazzi.spatialite.database.spatial.core.mbtiles; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Formatter; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.locks.ReentrantReadWriteLock; import eu.geopaparazzi.library.database.GPLog; import eu.geopaparazzi.spatialite.database.spatial.core.mbtiles.MbTilesMetadata.MetadataParseException; import eu.geopaparazzi.spatialite.database.spatial.core.mbtiles.MbTilesMetadata.MetadataValidator; public class MBTilesDroidSpitter { private SQLiteDatabase db_mbtiles = null; private File file_mbtiles; String s_mbtiles_file; String s_name; private MbTilesMetadata metadata = null; private String s_metadataVersion = "1.1"; private String s_tile_row_type = "tms"; private String s_center_parm = ""; private int i_type_tiles = -1; // mbtiles is only valid if 'i_type_tiles' == 0 or 1 [table or // view] private boolean b_grid_id = false; private int i_request_url_count = -1; // > 0 table 'request_url' exists public static final int i_request_url_count_read_value = 0; public static final int i_request_url_count_read_db = 1; public static final int i_request_url_count_create = 2; public static final int i_request_url_count_drop = 3; public static final int i_request_url_count_insert = 4; public static final int i_request_url_count_delete = 5; private boolean b_mbtiles_valid = false; private HashMap<String, String> mbtiles_metadata = null; HashMap<String, String> bounds_lat_long = null; // avoid SpatialiteLockException's - multiple read/writes will be queued private ReentrantReadWriteLock db_lock = new ReentrantReadWriteLock(); // ----------------------------------------------- /** * Constructor MBTilesDroidSpitter * <p/> * <ul> * <i>if the file does not exist, a valid mbtile database will be created</i> * <i>if the parent directory does not exist, it will be created</i> * </ul> * * @param file_mbtiles mbtiles.db file to open * @param mbtiles_metadata list of initial metadata values to set upon creation [otherwise can be null] */ public MBTilesDroidSpitter(File file_mbtiles, HashMap<String, String> mbtiles_metadata) { this.file_mbtiles = file_mbtiles; if (mbtiles_metadata == null) this.mbtiles_metadata = new LinkedHashMap<String, String>(); else this.mbtiles_metadata = mbtiles_metadata; // GPLog.androidLog(-1,"MBTilesDroidSpitter[" + file_mbtiles.getAbsolutePath() + "]"); if (!this.file_mbtiles.exists()) { // if the parent directory does not exist, it will be // created // - a mbtiles database will be created with default // values and closed try { if (!this.file_mbtiles.getName().endsWith(".mbtiles")) { // .mbtiles files must have // an .mbtiles extention, // force this String s_mbtiles_path = file_mbtiles.getParentFile().getAbsolutePath(); s_name = this.file_mbtiles.getName().substring(0, this.file_mbtiles.getName().lastIndexOf(".")); this.file_mbtiles = new File(s_mbtiles_path + "/" + s_name + ".mbtiles"); } create_mbtiles(this.file_mbtiles); } catch (IOException e) { GPLog.error(this, "MBTilesDroidSpitter[" + file_mbtiles.getAbsolutePath() + "]", e); } } this.s_name = this.file_mbtiles.getName().substring(0, this.file_mbtiles.getName().lastIndexOf(".")); this.s_mbtiles_file = file_mbtiles.getAbsolutePath(); } // ----------------------------------------------- /** * Open mbtiles Database * * @param fetchMetadata 1: fetch and load the mbtiles metaadata * @return void */ public void open(boolean fetchMetadata, String metadataVersion) { if (!metadataVersion.equals("")) this.s_metadataVersion = metadataVersion; try { db_mbtiles = SQLiteDatabase.openOrCreateDatabase(file_mbtiles, null); } catch (Exception e) { // try to open it readonly db_mbtiles = SQLiteDatabase.openDatabase(file_mbtiles.getAbsolutePath(), null, SQLiteDatabase.OPEN_READONLY); } if (!fetchMetadata) return; try { fetchMetadata(this.s_metadataVersion); } catch (MetadataParseException e) { GPLog.androidLog(4, "MBTilesDroidSpitter[" + file_mbtiles.getAbsolutePath() + "]", e); } // if (!isValid()) { // this mbtiles file is invalid // } } // ----------------------------------------------- /** * Close mbtiles Database * * @return void */ public void close() { if (db_mbtiles != null) db_mbtiles.close(); } // ----------------------------------------------- /** * Retrieve SQLiteDatabase connection * * @return SQLiteDatabase connection of mbtiles.db */ public SQLiteDatabase getmbtiles() { return db_mbtiles; } // ----------------------------------------------- /** * Return long name of map/file * <p/> * <p>default: file name with path and extention * <p>mbtiles : will be a '.mbtiles' sqlite-file-name * <p>map : will be a mapforge '.map' file-name * * @return file_map.getAbsolutePath(); */ public String getFileNamePath() { return this.s_mbtiles_file; // file_map.getAbsolutePath(); } // ----------------------------------------------- /** * Return short name of map/file * <p/> * <p>default: file name without path and extention * <p>mbtiles : metadata 'name' * <p>map : will be value of 'comment', if not null * * @return s_name as short name of map/file */ public String getName() { return this.s_name; // comment or file-name without path and extention } // ----------------------------------------------- /** * Return list of all zoom-levels and Bounds in LatLong * - last entry: min/max zoom-levels and Bounds * - this is calculated from the Database and will update the metadata-table * * @return bounds_lat_long list of zoom-levels and Bounds in LatLong */ public HashMap<String, String> getBoundsZoomLevels() { if (bounds_lat_long == null) { int i_reload_metadata = 1; fetch_bounds_minmax(i_reload_metadata, 1); } return bounds_lat_long; } // ----------------------------------------------- /** * Return center position with zoom-level * - entry: from metatable * * @return Center as LatLong and default zoom-level [13.37771496361961,52.51628011262304,17] */ public String getCenterParms() { return this.s_center_parm; } // ----------------------------------------------- /** * Function to retrieve Tile Drawable Bitmap from the mbtiles Database * - 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 * @return Drawable Bitmap of the tile or null if no tile matched the given parameters */ public Drawable getTileAsDrawable(int i_x, int i_y_osm, int i_z) { return new BitmapDrawable(getTileAsBitmap(i_x, i_y_osm, i_z)); } // ----------------------------------------------- /** * Function to retrieve Tile Bitmap from the mbtiles Database * - 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 * @return Bitmap of the tile or null if no tile matched the given parameters */ public Bitmap getTileAsBitmap(int i_x, int i_y_osm, int i_z) { // TODO: Optimize this if we have mbtiles_metadata with bound or min/max zoomlevels // Do not make any request and return null if we know it won't match any tile byte[] bb = getTileAsBytes(i_x, i_y_osm, i_z); return BitmapFactory.decodeByteArray(bb, 0, bb.length); } // ----------------------------------------------- /** * Function to retrieve Tile byte[] from the mbtiles Database * - i_y_osm must be in is Open-Street-Map 'Slippy Map' notation [will be converted to 'tms' notation if needed] * - first of two function that use the 'tms' numbering for the y/tile_row value * * @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 * @return byte[] of the tile to be used to create a Bitmap or null if no tile matched the given parameters */ public byte[] getTileAsBytes(int i_x, int i_y_osm, int i_z) { int i_y = i_y_osm; String s_x = ""; String s_y = ""; String s_z = ""; if (s_tile_row_type.equals("tms")) { int[] tmsTileXY = MBTilesDroidSpitter.googleTile2TmsTile(i_x, i_y_osm, i_z); i_y = tmsTileXY[1]; } try { s_x = Integer.toString(i_x); s_y = Integer.toString(i_y); s_z = Integer.toString(i_z); } catch (NumberFormatException e) { return null; } // db_lock.readLock().lock(); byte[] blob_data = null; try { final Cursor c = db_mbtiles.rawQuery( "select tile_data from tiles where tile_column=? and tile_row=? and zoom_level=?", new String[]{s_x, s_y, s_z}); if (!c.moveToFirst()) { c.close(); // db_lock.readLock().unlock(); return null; } blob_data = c.getBlob(c.getColumnIndex("tile_data")); c.close(); } catch (Exception e) { GPLog.error(this, null, e); //} finally { // causes crash // db_lock.readLock().unlock(); } return blob_data; } // ----------------------------------------------- /** * Function to insert a new Tile Bitmap to the mbtiles Database * - i_y_osm must be in is Open-Street-Map 'Slippy Map' notation [will be converted to 'tms' notation if needed] * - checking will be done to determin if the Bitmap is blank [i.e. all pixels have the same RGB] * * @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 i_force_unique 1=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 i_force_unique) throws IOException { int i_rc = 0; // i_parm=1: 'ff-ee-dd.rgb' [to be used as tile_id], blank if image is not Blank (all pixels // use one RGB value) String s_tile_id = get_pixel_rgb_toString(tile_bitmap, 1); ByteArrayOutputStream ba_stream = new ByteArrayOutputStream(); try { if (this.mbtiles_metadata.get("format") == "png") { // 'png' should be avoided, can // create very big databases tile_bitmap.compress(Bitmap.CompressFormat.PNG, 100, ba_stream); } else { // 'jpg' should be used where possible tile_bitmap.compress(Bitmap.CompressFormat.JPEG, 75, ba_stream); } byte[] ba_tile_data = ba_stream.toByteArray(); i_rc = insertTile(s_tile_id, i_x, i_y_osm, i_z, ba_tile_data, i_force_unique); } catch (Exception e) { i_rc = 1; GPLog.error(this, "MBTilesDroidSpitter[" + file_mbtiles.getAbsolutePath() + "]", e); } // GPLog.androidLog(-1,"MBTilesDroidSpitter.insertBitmapTile: inserting["+i_z+"/"+i_x+"/"+i_y_osm+"] rc=["+i_rc+"]"); return i_rc; } // ----------------------------------------------- /** * Function to insert a new Tile byte-data to the mbtiles Database * - i_y_osm must be in is Open-Street-Map 'Slippy Map' notation [will be converted to 'tms' notation if needed] * - second of two function that use the 'tms' numbering for the y/tile_row value * - blank [i.e. all pixels have the same RGB] image will only be saved once in the'images' table and reference in the 'map' table * * @param s_tile_id for images/map tables tile_id field [only filled when Bitmap is blank [all pixels haves ame rgb] * @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 ba_tile_data the image-data extracted from the Bitmap. * @param i_force_unique 1=check if image is unique in Database [may be slow if used] * @return 0: no error */ private int insertTile(String s_tile_id, int i_x, int i_y_osm, int i_z, byte[] ba_tile_data, int i_force_unique) throws IOException { // i_rc=0: correct, otherwise error int i_rc = 0; if (!isValid()) { // this mbtiles file is invalid return 100; // invalid mbtiles } boolean b_unique = true; if (s_tile_id.equals("")) { s_tile_id = get_tile_id_from_zxy(i_z, i_x, i_y_osm); } else { // This should be a 'Blank' Image :'ff-ee-dd.rgb', check if allready stored in // 'images' table b_unique = search_blank_image(s_tile_id); } int i_y = i_y_osm; if (s_tile_row_type.equals("tms")) { int[] tmsTileXY = MBTilesDroidSpitter.googleTile2TmsTile(i_x, i_y_osm, i_z); i_y = tmsTileXY[1]; } if (i_force_unique > 1) i_force_unique = 0; String s_images_tablename = "images"; String s_map_tablename = "map"; String s_tiles_tablename = "tiles"; String s_mbtiles_field_tile_id = "tile_id"; String s_mbtiles_field_grid_id = "grid_id"; String s_mbtiles_field_tile_data = "tile_data"; String s_mbtiles_field_zoom_level = "zoom_level"; String s_mbtiles_field_tile_column = "tile_column"; String s_mbtiles_field_tile_row = "tile_row"; String s_grid_id = ""; // The use of 'i_force_unique == 1' will probely slow things down to a craw // GPLog.androidLog(1,"insertTile tile_id["+s_tile_id+"] force_unique["+i_force_unique+"] unique["+b_unique+"]"); if ((i_force_unique == 1) && (b_unique)) { // mj10777: not yet properly tested: // - query the images table, searching for // 'ba_tile_data' // -- if found: // --- set 'b_unique=false;' // --- replace 's_tile_id' with images.tile_id of // found record String s_tile_id_query = ""; try { s_tile_id_query = search_tile_image(ba_tile_data); } catch (Exception e) { GPLog.error(this, null, e); i_rc = 1; } if (s_tile_id_query != "") { // We have this image, do not add again b_unique = false; // replace the present tile_id with the found referenced tile_id // the 'map' table will now reference the existing image in 'images' s_tile_id = s_tile_id_query; } } // The Database may have been closed in the meantime, we just don't know that yet - should // bail out gracefully // - avoid 'IllegalStateException' '(conn# x): already closed' if (db_mbtiles.isOpen()) { // You cannot lock the Database if the connection is not open db_lock.writeLock().lock(); db_mbtiles.beginTransaction(); try { if (b_unique) { // We do not have this image, add it if (i_type_tiles == 1) { ContentValues image_values = new ContentValues(); image_values.put(s_mbtiles_field_tile_data, ba_tile_data); image_values.put(s_mbtiles_field_tile_id, s_tile_id); db_mbtiles.insertOrThrow(s_images_tablename, null, image_values); } } if (i_type_tiles == 1) { // 'tiles' is a view // Note: the 'map' table will/should only reference an // existing image in the 'images'. // table // - it is possible that there is more than one reference // to an existing image // -- sample: an area has 15 tiles of one color (all pixels // of the tile have the same // RGB) // --- this image will be stored 1 time in 'images', but // will be used 15 times in 'map' ContentValues map_values = new ContentValues(); map_values.put(s_mbtiles_field_zoom_level, i_z); map_values.put(s_mbtiles_field_tile_column, i_x); map_values.put(s_mbtiles_field_tile_row, i_y); map_values.put(s_mbtiles_field_tile_id, s_tile_id); if (b_grid_id) map_values.put(s_mbtiles_field_grid_id, s_grid_id); db_mbtiles.insertOrThrow(s_map_tablename, null, map_values); } if (i_type_tiles == 0) { // 'tiles' is a table ContentValues tiles_values = new ContentValues(); tiles_values.put(s_mbtiles_field_zoom_level, i_z); tiles_values.put(s_mbtiles_field_tile_column, i_x); tiles_values.put(s_mbtiles_field_tile_row, i_y); tiles_values.put(s_mbtiles_field_tile_data, ba_tile_data); db_mbtiles.insertOrThrow(s_tiles_tablename, null, tiles_values); } db_mbtiles.setTransactionSuccessful(); } catch (Exception e) { int i_catch_rc = 0; if (e.getMessage() != null) { String s_message = e.getMessage(); if (s_message.contains("code 19")) { // When the tile // allready exists: // not to be // considered an // error i_rc = 0; // this will delete the request_url entry i_catch_rc = 19; } } if (i_catch_rc == 0) { throw new IOException("MBTilesDroidSpitter:insertTile error[" + e.getLocalizedMessage() + "] rc=" + i_rc); } } finally { db_mbtiles.endTransaction(); db_lock.writeLock().unlock(); int i_update = 1; try { // if the bounds or min/max zoom have changed, update changed values and // reload // metadata i_update = checkBounds(i_x, i_y_osm, i_z, i_update); // i_update=0: inside bounds ; othewise bounds and metadata have changed - not // used } catch (Exception e) { GPLog.error(this, null, e); i_rc = 1; } } } else { // '(conn# x): already closed' i_rc = 1; return i_rc; } // GPLog.androidLog(-1,"MBTilesDroidSpitter.insertTile: inserted["+i_z+"/"+i_x+"/"+i_y_osm+"] rc=["+i_rc+"]"); return i_rc; } // ----------------------------------------------- /** * Function to check if image is blank * - avoids duplicate images * * @param s_tile_id the image tile_id * @return true if unique or false if tile_id was found */ private boolean search_blank_image(String s_tile_id) throws IOException { boolean b_unique = true; if (i_type_tiles != 1) { // there will be no 'images' table, return return b_unique; } String s_sql_query = "SELECT count(tile_id) AS count_tile_id FROM images WHERE (tile_id = '" + s_tile_id + "')"; // mj10777: A good wms-server to test 'blank-images' (i.e. all pixels of image have the // same RGB) is: // http://fbinter.stadt-berlin.de/fb/wms/senstadt/ortsteil?LAYERS=0&FORMAT=image/jpeg&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&STYLES=visual&SRS=EPSG:4326&BBOX=XXX,YYY,XXX,YYY&WIDTH=256&HEIGHT=256 // SELECT count(tile_id) FROM images where tile_id like '%.rgb' : 8 // SELECT count(tile_id) FROM map where tile_id like '%.rgb' : 177 db_lock.readLock().lock(); try { final Cursor c = db_mbtiles.rawQuery(s_sql_query, null); if (c != null) { if (c.moveToFirst()) { int i_count_tile_id = c.getInt(c.getColumnIndex("count_tile_id")); if (i_count_tile_id > 0) { // We have this image, do not add again b_unique = false; } } c.close(); } } catch (Exception e) { String s_message = e.getMessage(); int index_of_text = s_message.indexOf("closed"); if (index_of_text != -1) { // attempt to re-open an already-closed [sometimes: 'already closed'] return false; } else { throw new IOException("MBTilesDroidSpitter:search_blank_image query[" + s_sql_query + "] error[" + e.getLocalizedMessage() + "] "); } } finally { db_lock.readLock().unlock(); } return b_unique; } // ----------------------------------------------- /** * Function to check if image exists in the image-table * - avoids duplicate images * * @param ba_tile_data the image-data extracted from the Bitmap. * @return tile_id of found image or blank */ private String search_tile_image(byte[] ba_tile_data) throws IOException { // The use of // 'i_force_unique // == 1' will // probely slow // things down to a // craw String s_tile_id = ""; if (i_type_tiles != 1) { // there will be no 'images' table, return return s_tile_id; } String s_tile_data = get_hex(ba_tile_data); String s_sql_query = "SELECT tile_id FROM images WHERE (hex(tile_data) = '" + s_tile_data + "')"; db_lock.readLock().lock(); try { // ?? another way to query for binary data in java ?? final Cursor c = db_mbtiles.rawQuery(s_sql_query, null); if (c != null) { if (c.moveToFirst()) { // TODO: do something if multiple results are returned s_tile_id = c.getString(c.getColumnIndex("tile_id")); } c.close(); } } catch (Exception e) { throw new IOException("MBTilesDroidSpitter:search_tile_image query[" + s_sql_query + "] error[" + e.getLocalizedMessage() + "] "); } finally { db_lock.readLock().unlock(); } if (s_tile_id != "") { String msg = "MBTilesDroidSpitter:search_tile_image[" + file_mbtiles.getAbsolutePath() + "] tile_id[" + s_tile_id + "] [a non-blank unique image has been found]"; if (GPLog.LOG_HEAVY) GPLog.addLogEntry("MBTilesDroidSpitter", msg); } return s_tile_id; } // ----------------------------------------------- /** * Function to check if inserted tile is outside known bounds and min/max zoom, update metadata if desired * - 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_update 1=updata metadate if outside of range [bounds, min/max zoom] * @return 1: metdata was updated */ public int checkBounds(int i_x, int i_y_osm, int i_z, int i_update) throws IOException { int i_rc = 0; // GPLog.androidLog(-1,"MBTilesDroidSpitter.icheckBounds: parms["+i_z+"/"+i_x+"/"+i_y_osm+"] i_fetch_bounds["+i_fetch_bounds+"]"); // minx, miny, maxx, maxy double[] tileBounds = tileLatLonBounds(i_x, i_y_osm, i_z, 256); HashMap<String, String> update_metadata = this.metadata.checkTileLocation(tileBounds, i_z); if (update_metadata.size() > 0) { if (i_update == 1) { // the bounds or min/max zoom have changed, update changed values // and reload metadata int i_reload_metadata = 1; // call fetchMetadata so that the new values will take // effect try { update_mbtiles_metadata(db_mbtiles, update_metadata, i_reload_metadata); } catch (Exception e) { GPLog.error(this, "MBTilesDroidSpitter[" + file_mbtiles.getAbsolutePath() + "]", e); } } i_rc = 1; } return i_rc; } // ----------------------------------------------- /** * Query the mbtiles metadata-table and returns validated results * - when called the first time, a mbtiles validity check is done * * @return HashMap<String,String> metadate [key,value] */ public MbTilesMetadata fetchMetadata(String metadataVersion) throws MetadataParseException { Cursor c = db_mbtiles.query(MbTilesSQLite.TABLE_METADATA, new String[]{MbTilesSQLite.COL_METADATA_NAME, MbTilesSQLite.COL_METADATA_VALUE}, null, null, null, null, null); MetadataValidator validator = MbTilesMetadata.MetadataValidatorFactory.getMetadataValidatorFromVersion(metadataVersion); if (validator == null) return null; this.metadata = MbTilesMetadata.createFromCursor(c, c.getColumnIndex(MbTilesSQLite.COL_METADATA_NAME), c.getColumnIndex(MbTilesSQLite.COL_METADATA_VALUE), validator); if (this.metadata != null) { // mbtiles is only valid if 'metadata' has values this.s_tile_row_type = this.metadata.s_tile_row_type; this.s_center_parm = this.metadata.s_center_parm; if (i_type_tiles < 0) { // mbtiles is only valid if 'i_type_tiles' == 0 or 1 [table or // view] i_type_tiles = check_type_tiles(); switch (i_type_tiles) { case 0: case 1: b_mbtiles_valid = true; break; default: b_mbtiles_valid = false; break; } } } return this.metadata; } // ----------------------------------------------- /** * Is the mbtiles file considerd valid * - metadata table exists and has data * - 'tiles' is either a table or a view and the correct fields exist * -- if a view: do the tables map and images exist with the correct fields * checking is done once when the 'metadata' is retrieved the first time [fetchMetadata()] * * @return b_mbtiles_valid true if valid, otherwise false */ public boolean isValid() { return b_mbtiles_valid; } // ----------------------------------------------- /** * Checks table type of 'tiles' [needed for an insert of a tile] * tiles : do the fields zoom_level [z], tile_column [x], tile_row [y] and tile_data exist * - both as 'view' or 'table' must have these fields * when tiles is a view: * - map : do the fields zoom_level [z], tile_column [x], tile_row [y] and tile_id exist * -- this table should also have a grid_id [but not needed for this implementation] * - images : do the fields tile_data and tile_id exist * with these checks, reading / writing should always work [if a netadata table exists, the mbtiles is valid] * * @return i_rc to set 'i_type_tiles' [view[1] or table[0]], anything else should be consider an error for inserting */ private int check_type_tiles() { int i_rc = -1; boolean b_mbtiles_valid = false; String s_type_tiles = ""; String s_field = ""; int i_field_count = 0; Cursor c_tiles = db_mbtiles.rawQuery("SELECT type AS type_tiles FROM sqlite_master WHERE tbl_name = 'tiles'", null); if ((c_tiles != null) && (c_tiles.getColumnCount() > 0) && (c_tiles.getCount() > 0)) { if (c_tiles.moveToFirst()) { s_type_tiles = c_tiles.getString(c_tiles.getColumnIndex("type_tiles")); } c_tiles.close(); } if (s_type_tiles.equals("view") || s_type_tiles.equals("table")) { // i_type_tiles<0 = // invalid mbtiles ; // 0='tiles' is a table ; // 1='tiles' is a view Cursor c_fields = db_mbtiles.rawQuery("pragma table_info(tiles)", null); if (c_fields != null) { if (c_fields.moveToFirst()) { do { // tiles : do the fields zoom_level [z], tile_column [x], tile_row [y] and // tile_data exist s_field = c_fields.getString(c_fields.getColumnIndex("name")); if ((s_field.equals("zoom_level")) || (s_field.equals("tile_column")) || (s_field.equals("tile_row")) || (s_field.equals("tile_data"))) { i_field_count++; } else { if (s_field.equals("grid_id")) { // not all mbtiles map tables have a // 'grid_id': avoid insert error b_grid_id = true; } } } while (c_fields.moveToNext()); } c_fields.close(); } if (i_field_count != 4) { // set as invalid s_type_tiles = ""; } if (s_type_tiles.equals("table")) { i_rc = 0; b_mbtiles_valid = true; } if (s_type_tiles.equals("view")) { // the view will reference 2 tables [map,images] with // specific fields i_field_count = 0; c_fields = db_mbtiles.rawQuery("pragma table_info(map)", null); if (c_fields != null) { if (c_fields.moveToFirst()) { do { // map : do the fields zoom_level [z], tile_column [x], tile_row [y] // and tile_id exist s_field = c_fields.getString(c_fields.getColumnIndex("name")); if ((s_field.equals("zoom_level")) || (s_field.equals("tile_column")) || (s_field.equals("tile_row")) || (s_field.equals("tile_id"))) { i_field_count++; } else { if (s_field.equals("grid_id")) { // not all mbtiles map tables have // a 'grid_id': avoid insert error b_grid_id = true; } } } while (c_fields.moveToNext()); } c_fields.close(); } if (i_field_count == 4) { i_field_count = 0; c_fields = db_mbtiles.rawQuery("pragma table_info(images)", null); if (c_fields != null) { if (c_fields.moveToFirst()) { do { // images : do the fields tile_data and tile_id exist s_field = c_fields.getString(c_fields.getColumnIndex("name")); if ((s_field.equals("tile_id")) || (s_field.equals("tile_data"))) { i_field_count++; } } while (c_fields.moveToNext()); } c_fields.close(); } if (i_field_count == 2) { i_rc = 1; b_mbtiles_valid = true; } } if (i_rc != 1) { s_type_tiles = ""; } } } if (b_mbtiles_valid) { get_request_url_count(i_request_url_count_read_db); // will read status from Database } return i_rc; } // ----------------------------------------------- /** * Retrieves a list of tile id requesed, based on bounds and zoom-level * return values values: * tile_id : created with: get_tile_id_from_zxy * - will be read and parsed with: get_zxy_from_tile_id in on_request_create_url * 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'] * @return ist_tile_id [list of 'tile_id' needed] */ public List<String> build_request_list(double[] request_bounds, int i_zoom_level, String s_request_type, String s_url_source, String s_request_y_type) { List<String> list_tile_id = new ArrayList<String>(); // this will be the final object - // depending on type if ((!s_request_type.equals("fill")) && (!s_request_type.equals("replace")) && (!s_request_type.equals("exists"))) s_request_type = "exists"; // set default, if invalid int[] tile_bounds = LatLonBounds_to_TileBounds(request_bounds, i_zoom_level); // i_zoom=tile_bounds[0]; int i_min_x = tile_bounds[1]; int i_min_y_osm = tile_bounds[2]; int i_max_x = tile_bounds[3]; int i_max_y_osm = tile_bounds[4]; int i_min_y_tms = i_min_y_osm; int i_max_y_tms = i_max_y_osm; int i_min_y = i_min_y_osm; int i_max_y = i_max_y_osm; int i_count_x = i_max_x - i_max_x; int i_count_y = i_max_y_osm - i_max_y_osm; if (s_tile_row_type.equals("tms")) { int[] tmsTileXY = MBTilesDroidSpitter.googleTile2TmsTile(i_min_x, i_min_y_osm, i_zoom_level); i_min_y_tms = tmsTileXY[1]; i_min_y = i_min_y_tms; tmsTileXY = MBTilesDroidSpitter.googleTile2TmsTile(i_max_x, i_max_y_osm, i_zoom_level); i_max_y_tms = tmsTileXY[1]; i_max_y = i_max_y_tms; } if ((s_request_type.equals("fill")) || (s_request_type.equals("replace"))) { // Sorted from // West to East // ; North to // South for (int x = i_min_x; x <= i_max_x; x++) { // fill the list will all possible // combinations : will be the result of // 'replace' // the osm number for north[max] is smaller // that south[min] for (int y = i_max_y_osm; y <= i_min_y_osm; y++) { // input must be y with // osm-notation - returned will // be the notation based on the // active database String s_tile_id = get_tile_id_from_zxy(i_zoom_level, x, y); if (!s_url_source.equals("")) { // We are adding from an existing set of tiles, // the tile must exist String s_file = s_url_source; int[] zxy_osm_tms = get_zxy_from_tile_id(s_tile_id); if ((zxy_osm_tms != null) && (zxy_osm_tms.length == 4)) { int i_z = zxy_osm_tms[0]; int i_x = zxy_osm_tms[1]; int i_y_osm = zxy_osm_tms[2]; int i_y_tms = zxy_osm_tms[3]; int i_y = i_y_osm; if (s_request_y_type.equals("tms")) { i_y = i_y_tms; } int indexOfZ = s_file.indexOf("ZZZ"); if (indexOfZ != -1) { // tile-server: replace ZZZ,XXX,YYY s_file = s_file.replaceFirst("ZZZ", String.valueOf(i_z)); //$NON-NLS-1$ s_file = s_file.replaceFirst("XXX", String.valueOf(i_x)); //$NON-NLS-1$ s_file = s_file.replaceFirst("YYY", String.valueOf(i_y)); //$NON-NLS-1$ File file_tile = new File(s_file); if (!file_tile.exists()) { // if the tile-file does not exist, do // not add s_tile_id = ""; } } } } if (!s_tile_id.equals("")) { list_tile_id.add(s_tile_id); } } } if (s_request_type.equals("replace")) { return list_tile_id; // returns all posibilities } } List<String> list_tile_id_exists = new ArrayList<String>(); String s_table = "map"; if (i_type_tiles == 0) { // mbtiles is only valid if 'i_type_tiles' == 0 or 1 [table or // view] s_table = "tiles"; // map will not exist } String s_select_where = "WHERE ((zoom_level = " + i_zoom_level + ") AND "; s_select_where = s_select_where + "((tile_column >= " + i_min_x + ") AND (tile_column <= " + i_max_x + ")) AND "; if (s_tile_row_type.equals("tms")) { // tms: Sorted from West to East ; North to South s_select_where = s_select_where + "((tile_row >= " + i_min_y + ") AND (tile_row <= " + i_max_y + "))) "; s_select_where = s_select_where + "ORDER BY tile_column ASC,tile_row DESC"; } else { // osm: Sorted from West to East ; North to South s_select_where = s_select_where + "((tile_row >= " + i_max_y + ") AND (tile_row <= " + i_min_y + "))) "; s_select_where = s_select_where + "ORDER BY tile_column ASC,tile_row ASC"; } String s_select_sql = "SELECT tile_column,tile_row FROM " + s_table + " " + s_select_where; // SELECT tile_column,tile_row FROM map // WHERE ((zoom_level = 15) AND // ((tile_column >= 1759) AND (tile_column <= 17608)) AND // ((tile_row >= 22013) AND (tile_row <= 22026))) // ORDER BY tile_column ASC,tile_row DESC db_lock.readLock().lock(); try { Cursor c_tiles = db_mbtiles.rawQuery(s_select_sql, null); if ((c_tiles != null) && (c_tiles.getColumnCount() > 0) && (c_tiles.getCount() > 0)) { if (c_tiles.moveToFirst()) { do { // map : do the fields zoom_level [z], tile_column [x], tile_row [y] and // tile_id exist int i_tile_column = c_tiles.getInt(c_tiles.getColumnIndex("tile_column")); int i_tile_row = c_tiles.getInt(c_tiles.getColumnIndex("tile_row")); String s_tile_id = i_zoom_level + "-" + i_tile_column + "-" + i_tile_row + "." + s_tile_row_type; list_tile_id_exists.add(s_tile_id); } while (c_tiles.moveToNext()); } } c_tiles.close(); } catch (Exception e) { GPLog.error(this, null, e); } finally { db_lock.readLock().unlock(); } // GPLog.androidLog(-1,"build_request_list["+s_request_type+"] all["+list_tile_id.size()+"] exists["+list_tile_id_exists.size()+"] sql["+s_select_sql+"]"); if (s_request_type.equals("exists")) { list_tile_id.clear(); return list_tile_id_exists; // returns all that exist } // 'fill' remove existing from the compleate list and return the missing for (String s_tile_id : list_tile_id_exists) { list_tile_id.remove(s_tile_id); } list_tile_id_exists.clear(); return list_tile_id; } // ----------------------------------------------- /** * Returns status of table: request_url * parm values: * 0: return existing value [set when database was opended,not reading the table] [MBTilesDroidSpitter.i_request_url_read_value] * 1 : return existing value return existing value [reading the table with count after checking if it exits] [MBTilesDroidSpitter.i_request_url_read_db] * 2: create table (if it does not exist) [MBTilesDroidSpitter.i_request_url_count_create] * 3: delete table (if it does exist) [MBTilesDroidSpitter.i_request_url_count_drop] * -- may be called within a '.beginTransaction()' * * @param i_parm type of result * @return i_request_url_count [< 0: table does not exist; 0=exist but is empty ; > 0 open requests] */ public int get_request_url_count(int i_parm) { String s_sql_request_url = ""; switch (i_parm) { case i_request_url_count_drop: { // create if '-1' or delete if '0', otherwise return count s_sql_request_url = "DROP TABLE IF EXISTS request_url"; try { db_mbtiles.execSQL(s_sql_request_url); } catch (Exception e) { s_sql_request_url = ""; GPLog.error(this, "MBTilesDroidSplitter: [" + getName() + "] -E-> get_request_url_count: parm[" + i_parm + "][2=DROP or CREATE request_url] get_request_url_count[" + this.i_request_url_count + "] sql[" + s_sql_request_url + "]", e); } finally { if (!s_sql_request_url.equals("")) { // no error [DROP or CREATE was compleated // correctly] // table has been deleted and is empty this.i_request_url_count = -1; } } } break; case i_request_url_count_create: { s_sql_request_url = "CREATE TABLE IF NOT EXISTS request_url (tile_id TEXT PRIMARY KEY,tile_url TEXT)"; try { db_mbtiles.execSQL(s_sql_request_url); } catch (Exception e) { s_sql_request_url = ""; GPLog.error(this, "MBTilesDroidSplitter: [" + getName() + "] -E-> get_request_url_count: parm[" + i_parm + "][2=DROP or CREATE request_url] get_request_url_count[" + this.i_request_url_count + "] sql[" + s_sql_request_url + "]", e); } finally { if (!s_sql_request_url.equals("")) { // no error [DROP or CREATE was compleated // correctly] // table has been created and is empty this.i_request_url_count = 0; } } } break; case i_request_url_count_read_db: { // read from database int i_field_count = 0; db_lock.readLock().lock(); try { s_sql_request_url = "pragma table_info(request_url)"; Cursor c_tiles = db_mbtiles.rawQuery(s_sql_request_url, null); if ((c_tiles != null) && (c_tiles.getColumnCount() > 0) && (c_tiles.getCount() > 0)) { // then // the // table // exists // [no // it // does // not! // fields // must // be // read] if ((c_tiles.getCount() > 0) && (c_tiles.moveToFirst())) { do { // tiles : do the fields zoom_level [z], tile_column [x], tile_row [y] // and tile_data exist String s_field = c_tiles.getString(c_tiles.getColumnIndex("name")); if ((s_field.equals("tile_id")) || (s_field.equals("tile_url"))) { i_field_count++; } } while (c_tiles.moveToNext()); } c_tiles.close(); if (i_field_count > 0) { this.i_request_url_count = 0; s_sql_request_url = "SELECT count(tile_id) AS count_id FROM request_url"; c_tiles = db_mbtiles.rawQuery(s_sql_request_url, null); if ((c_tiles != null) && (c_tiles.getColumnCount() > 0) && (c_tiles.getCount() > 0)) { if (c_tiles.moveToFirst()) { this.i_request_url_count = c_tiles.getInt(c_tiles.getColumnIndex("count_id")); } } c_tiles.close(); } else { this.i_request_url_count = -1; } } } catch (Exception e) { GPLog.error(this, "MBTilesDroidSplitter: [" + getName() + "] -E-> get_request_url_count: parm[" + i_parm + "][1=pragma table_info(request_url) or count(tile_id)] get_request_url_count[" + this.i_request_url_count + "] sql[" + s_sql_request_url + "]", e); } finally { db_lock.readLock().unlock(); } } break; default: case i_request_url_count_read_value: { // return existing value } break; } return this.i_request_url_count; } // ----------------------------------------------- /** * insert/delete of record in table: request_url * parm values: * 3: insert record with: s_tile_id and s_tile_url [MBTilesDroidSpitter.i_request_url_count_insert] * - 'request_url' will be created if it does not exist * 1: delete record with: s_tile_id, delete table if count is 0 * 4: delete record with: s_tile_id, delete table if count is 0 [MBTilesDroidSpitter.i_request_url_count_delete] * - 'request_url' will be deleted when the last records is deleted * may be called within a '.beginTransaction()' * * @param i_parm type of command * @param s_tile_id tile_id to use * @param s_tile_url full url to retrieve tile with * @return i_request_url_count [< 0: table does not exist; 0=exist but is empty ; > 0 open requests] */ public int insert_request_url(int i_parm, String s_tile_id, String s_tile_url) { String s_sql_request_url = ""; switch (i_parm) { case i_request_url_count_delete: { // delete s_sql_request_url = "DELETE FROM 'request_url' WHERE ( tile_id = '" + s_tile_id + "')"; try { // no lock/unlock here, done in 'insert_list_request_url' db_mbtiles.execSQL(s_sql_request_url); this.i_request_url_count--; } catch (Exception e) { GPLog.error(this, null, e); } finally { if (this.i_request_url_count < 1) { // this will delete the empty table this.i_request_url_count = 0; get_request_url_count(i_request_url_count_drop); } } } break; case i_request_url_count_insert: { // insert if (this.i_request_url_count < 0) { // this will create the table get_request_url_count(i_request_url_count_create); } // s_sql_request_url = // "INSERT OR REPLACE INTO 'request_url' VALUES('"+s_tile_id+"','"+s_tile_url+"')"; ContentValues tiles_values = new ContentValues(); tiles_values.put("tile_id", s_tile_id); tiles_values.put("tile_url", s_tile_url); try { // no lock/unlock here, done in 'insert_list_request_url' db_mbtiles.insertOrThrow("request_url", null, tiles_values); // will only be added if it did not exist this.i_request_url_count++; } catch (Exception e) { GPLog.error(this, null, e); } } break; } // GPLog.androidLog(-1,"MBTilesDroidSplitter: ["+getName()+"] -I-> insert_request_url: parm["+i_parm+"][3=INSERT;4=DELETE] tile_id["+s_tile_id+"]"); return this.i_request_url_count; } // ----------------------------------------------- /** * bulk insert of records in table: request_url * - the request_url table will be created if it does not exist * - inserting is done for each Zoom-Level * - '.beginTransaction()' and '.endTransaction()' [and, of course: '.setTransactionSuccessful();'] * * @return i_request_url_count [< 0: table does not exist; 0=exist but is empty ; > 0 open requests] */ public int insert_list_request_url(HashMap<String, String> mbtiles_request_url) { int i_request_url_count_prev = this.i_request_url_count; db_lock.writeLock().lock(); db_mbtiles.beginTransaction(); try { for (Map.Entry<String, String> request_url : mbtiles_request_url.entrySet()) { String s_tile_id = request_url.getKey(); String s_tile_url = request_url.getValue(); insert_request_url(i_request_url_count_insert, s_tile_id, s_tile_url); } db_mbtiles.setTransactionSuccessful(); } catch (Exception e) { GPLog.error(this, "MBTilesDroidSplitter: [" + getName() + "] -E-> insert_list_request_url[" + this.i_request_url_count + "] mbtiles_request_url.size[" + mbtiles_request_url.size() + "] empty[" + mbtiles_request_url.isEmpty() + "]", e); } finally { db_mbtiles.endTransaction(); db_lock.writeLock().unlock(); } // GPLog.androidLog(-1,"["+getName()+"] insert_list_request_url["+this.i_request_url_count+"] "); return this.i_request_url_count; } // ----------------------------------------------- /** * House-keeping tasks for Database * The ANALYZE command gathers statistics fragment_about tables and indices * The VACUUM command rebuilds the entire database. * - 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; db_lock.writeLock().lock(); try { i_rc = 2; db_mbtiles.execSQL("VACUUM"); i_rc = 1; // db_mbtiles.execSQL("ANALYZE"); // ANALYZE i_rc = 0; } catch (Exception e) { GPLog.error(this, "MBTilesDroidSplitter: [" + getName() + "] -E-> on_analyze_vacuum[" + i_rc + "] ", e); } finally { db_lock.writeLock().unlock(); GPLog.androidLog(-1, "MBTilesDroidSplitter: [" + getName() + "] -I-> on_analyze_vacuum[" + i_rc + "] "); } return i_rc; } // ----------------------------------------------- /** * Returns list of collected 'request_url' * - Query only when 'this.i_request_url_count' > 0 ; i.e. Table exists and has records * * @param i_limit amount of records to retrieve [i_limit < 1 == all] * @return HashMap<String,String> mbtiles_request_url [tile_id,tile_url] */ public HashMap<String, String> retrieve_request_url(int i_limit) { HashMap<String, String> mbtiles_request_url = new LinkedHashMap<String, String>(); String s_limit = ""; if ((i_limit > 0) && (i_limit < this.i_request_url_count)) { // avoid excesive memory usage s_limit = " LIMIT " + i_limit; } if (this.i_request_url_count > 0) { db_lock.readLock().lock(); try { String s_mbtiles_request_url = "SELECT tile_id,tile_url FROM request_url" + s_limit; Cursor c_tiles = db_mbtiles.rawQuery(s_mbtiles_request_url, null); if ((c_tiles != null) && (c_tiles.getColumnCount() > 0) && (c_tiles.getCount() > 0)) { // avoid // CursorIndexOutOfBoundsException if ((c_tiles.getCount() > 0) && (c_tiles.moveToFirst())) { do { String s_tile_id = c_tiles.getString(c_tiles.getColumnIndex("tile_id")); String s_tile_url = c_tiles.getString(c_tiles.getColumnIndex("tile_url")); mbtiles_request_url.put(s_tile_id, s_tile_url); } while (c_tiles.moveToNext()); } c_tiles.close(); } } catch (Exception e) { GPLog.error(this, "MBTilesDroidSplitter: [" + getName() + "] -E-> retrieve_request_url[" + this.i_request_url_count + "] ", e); } finally { db_lock.readLock().unlock(); } } return mbtiles_request_url; } // ----------------------------------------------- /** * Returns result of last called fetchMetadata * * @return HashMap<String,String> metadate [key,value] */ public MbTilesMetadata getMetadata() { return this.metadata; } // ----------------------------------------------- /** * Function to create new mbtiles Database * - parent diretories will be created, if needed * - needed Tables/View and default values for metdata-table will be created * * @param file_mbtiles name of Database file * @return 0: no error */ public int create_mbtiles(File file_mbtiles) throws IOException { int i_rc = 0; File dir_mbtiles = file_mbtiles.getParentFile(); String mbtiles_name = file_mbtiles.getName().substring(0, file_mbtiles.getName().lastIndexOf(".")); if (!dir_mbtiles.exists()) { if (!dir_mbtiles.mkdir()) { throw new IOException("MBTilesDroidSpitter: create_mbtiles: mbtiles_dir[" + dir_mbtiles.getAbsolutePath() + "] creation failed"); } } SQLiteDatabase sqlite_db = SQLiteDatabase.openOrCreateDatabase(file_mbtiles, null); if (sqlite_db != null) { sqlite_db.setLocale(Locale.getDefault()); sqlite_db.setLockingEnabled(false); // CREATE TABLES and default values try { // create default tables for mbtiles // set default values for metadata, where not supplied in 'this.mbtiles_metadata' create_mbtiles_tables(sqlite_db, mbtiles_name); // write all metatdata values to database (with 'null', 'this.mbtiles_metadata' will // be used) int i_reload_metadata = 0; // do not reload, since we are closing the db update_mbtiles_metadata(sqlite_db, null, i_reload_metadata); } catch (Exception e) { sqlite_db.close(); sqlite_db = null; GPLog.error(this, "MBTilesDroidSpitter[" + file_mbtiles.getAbsolutePath() + "]", e); i_rc = 2; return i_rc; } sqlite_db.close(); sqlite_db = null; } return i_rc; } // ----------------------------------------------- /** * Function to create needed mbtiles Tables/Views * - default values will be added to this.mbtiles_metadata where needed * * @param mbtiles_db Database connection * @param mbtiles_name used for metadata.name if otherwise not set in this.mbtiles_metadata * @return 0: no error */ public int create_mbtiles_tables(SQLiteDatabase mbtiles_db, String mbtiles_name) throws IOException { int i_rc = 0; String s_mbtiles_field_tile_data = "tile_data"; String s_mbtiles_field_zoom_level = "zoom_level"; String s_mbtiles_field_tile_column = "tile_column"; String s_mbtiles_field_tile_row = "tile_row"; String s_mbtiles_field_tile_id = "tile_id"; String s_mbtiles_field_grid_id = "grid_id"; String s_mbtiles_field_name = "name"; String s_mbtiles_field_value = "value"; String s_metadata_tablename = "metadata"; String s_images_tablename = "images"; String s_tiles_tablename = "tiles"; String s_map_tablename = "map"; // ----------------------------------------------- String s_sql_create_grid_key = "CREATE TABLE IF NOT EXISTS grid_key (" + s_mbtiles_field_grid_id + " TEXT,key_name TEXT)"; String s_sql_create_grid_utfgrid = "CREATE TABLE IF NOT EXISTS grid_utfgrid (" + s_mbtiles_field_grid_id + " TEXT,grid_utfgrid BLOB)"; String s_sql_create_images = "CREATE TABLE IF NOT EXISTS " + s_images_tablename + " (" + s_mbtiles_field_tile_data + " blob," + s_mbtiles_field_tile_id + " text)"; String s_sql_create_keymap = "CREATE TABLE IF NOT EXISTS keymap (key_name TEXT,key_json TEXT)"; String s_sql_create_map = "CREATE TABLE IF NOT EXISTS " + s_map_tablename + " (" + s_mbtiles_field_zoom_level + " INTEGER," + s_mbtiles_field_tile_column + " INTEGER," + s_mbtiles_field_tile_row + " INTEGER," + s_mbtiles_field_tile_id + " TEXT," + s_mbtiles_field_grid_id + " TEXT)"; String s_sql_create_metadata = "CREATE TABLE IF NOT EXISTS " + s_metadata_tablename + " (" + s_mbtiles_field_name + " text," + s_mbtiles_field_value + " text)"; // CREATE VIEW tiles AS SELECT map.zoom_level AS zoom_level,map.tile_column AS // tile_column,map.tile_row AS tile_row,images.tile_data AS tile_data FROM map JOIN images // ON images.tile_id = map.tile_id ORDER BY zoom_level,tile_column,tile_row String s_sql_create_view_tiles = "CREATE VIEW IF NOT EXISTS " + s_tiles_tablename + " AS SELECT " + s_map_tablename + "." + s_mbtiles_field_zoom_level + " AS " + s_mbtiles_field_zoom_level + "," + s_map_tablename + "." + s_mbtiles_field_tile_column + " AS " + s_mbtiles_field_tile_column + "," + s_map_tablename + "." + s_mbtiles_field_tile_row + " AS " + s_mbtiles_field_tile_row + "," + s_images_tablename + "." + s_mbtiles_field_tile_data + " AS " + s_mbtiles_field_tile_data + " FROM " + s_map_tablename + " JOIN " + s_images_tablename + " ON " + s_images_tablename + "." + s_mbtiles_field_tile_id + " = " + s_map_tablename + "." + s_mbtiles_field_tile_id + " ORDER BY " + s_mbtiles_field_zoom_level + "," + s_mbtiles_field_tile_column + "," + s_mbtiles_field_tile_row; String s_sql_create_view_grids = "CREATE VIEW IF NOT EXISTS grids AS SELECT " + s_map_tablename + "." + s_mbtiles_field_zoom_level + " AS " + s_mbtiles_field_zoom_level + "," + s_map_tablename + "." + s_mbtiles_field_tile_column + " AS " + s_mbtiles_field_tile_column + "," + s_map_tablename + "." + s_mbtiles_field_tile_row + " AS " + s_mbtiles_field_tile_row + ",grid_utfgrid.grid_utfgrid AS grid FROM " + s_map_tablename + " JOIN grid_utfgrid ON grid_utfgrid." + s_mbtiles_field_grid_id + " = " + s_map_tablename + "." + s_mbtiles_field_grid_id; String s_sql_create_view_grid_data = "CREATE VIEW IF NOT EXISTS grid_data AS SELECT " + s_map_tablename + "." + s_mbtiles_field_zoom_level + " AS " + s_mbtiles_field_zoom_level + "," + s_map_tablename + "." + s_mbtiles_field_tile_column + " AS " + s_mbtiles_field_tile_column + "," + s_map_tablename + "." + s_mbtiles_field_tile_row + " AS " + s_mbtiles_field_tile_row + ",keymap.key_name AS key_name,keymap.key_json AS key_json FROM " + s_map_tablename + " JOIN grid_key ON " + s_map_tablename + "." + s_mbtiles_field_grid_id + " = grid_key." + s_mbtiles_field_grid_id + " JOIN keymap ON grid_key.key_name = keymap.key_name"; String s_sql_create_index_grid_key_lookup = "CREATE UNIQUE INDEX IF NOT EXISTS grid_key_lookup ON grid_key (" + s_mbtiles_field_grid_id + ",key_name)"; String s_sql_create_index_grid_utfgrid_lookup = "CREATE UNIQUE INDEX IF NOT EXISTS grid_utfgrid_lookup ON grid_utfgrid (" + s_mbtiles_field_grid_id + ")"; String s_sql_create_index_images = "CREATE UNIQUE INDEX IF NOT EXISTS " + s_images_tablename + "_id ON " + s_images_tablename + " (" + s_mbtiles_field_tile_id + " )"; String s_sql_create_index_keymap_lookup = "CREATE UNIQUE INDEX IF NOT EXISTS keymap_lookup ON keymap (key_name)"; String s_sql_create_index_map = "CREATE UNIQUE INDEX IF NOT EXISTS " + s_map_tablename + "_index ON " + s_map_tablename + " (" + s_mbtiles_field_zoom_level + "," + s_mbtiles_field_tile_column + "," + s_mbtiles_field_tile_row + ")"; String s_sql_create_index_metadata = "CREATE UNIQUE INDEX IF NOT EXISTS " + s_metadata_tablename + "_index ON " + s_metadata_tablename + " (" + s_mbtiles_field_name + ")"; // String // s_sql_create_android_metadata="CREATE TABLE IF NOT EXISTS android_"+s_metadata_tablename+" (locale text)"; // mj10777: not needed in android - done with sqlite_db.setLocale(Locale.getDefault()); // ---------------------------------------------- db_lock.writeLock().lock(); mbtiles_db.beginTransaction(); try { mbtiles_db.execSQL(s_sql_create_grid_key); mbtiles_db.execSQL(s_sql_create_grid_utfgrid); mbtiles_db.execSQL(s_sql_create_images); mbtiles_db.execSQL(s_sql_create_keymap); mbtiles_db.execSQL(s_sql_create_map); mbtiles_db.execSQL(s_sql_create_metadata); mbtiles_db.execSQL(s_sql_create_view_tiles); mbtiles_db.execSQL(s_sql_create_view_grids); mbtiles_db.execSQL(s_sql_create_view_grid_data); mbtiles_db.execSQL(s_sql_create_index_grid_key_lookup); mbtiles_db.execSQL(s_sql_create_index_grid_utfgrid_lookup); mbtiles_db.execSQL(s_sql_create_index_images); mbtiles_db.execSQL(s_sql_create_index_keymap_lookup); mbtiles_db.execSQL(s_sql_create_index_map); mbtiles_db.execSQL(s_sql_create_index_metadata); // mbtiles_db.execSQL(s_sql_create_android_metadata); mbtiles_db.setTransactionSuccessful(); } catch (Exception e) { i_rc = 1; throw new IOException("MBTilesDroidSpitter: create_mbtiles_tables error[" + e.getLocalizedMessage() + "]"); } finally { mbtiles_db.endTransaction(); db_lock.writeLock().unlock(); } // ---------------------------------------------- if (this.mbtiles_metadata.get("type") == null) this.mbtiles_metadata.put("type", "baselayer"); if (this.mbtiles_metadata.get("tile_row_type") == null) { this.mbtiles_metadata.put("tile_row_type", s_tile_row_type); } else { this.s_tile_row_type = this.mbtiles_metadata.get("tile_row_type"); } if (this.mbtiles_metadata.get("version") == null) this.mbtiles_metadata.put("version", "1.1"); if (this.mbtiles_metadata.get("format") == null) this.mbtiles_metadata.put("format", "jpg"); if (this.mbtiles_metadata.get("name") == null) this.mbtiles_metadata.put("name", mbtiles_name); if (this.mbtiles_metadata.get("description") == null) this.mbtiles_metadata.put("description", mbtiles_name); if (this.mbtiles_metadata.get("bounds") == null) this.mbtiles_metadata.put("bounds", "-180.0,-85.05113,180.0,85.05113"); if (this.mbtiles_metadata.get("center") == null) this.mbtiles_metadata.put("center", "0.0,0.0,1"); if (this.mbtiles_metadata.get("minzoom") == null) this.mbtiles_metadata.put("minzoom", "1"); if (this.mbtiles_metadata.get("maxzoom") == null) this.mbtiles_metadata.put("maxzoom", "1"); // ---------------------------------------------- return i_rc; } // ----------------------------------------------- /** * General Function to update mbtiles metadata Table * * @param mbtiles_db Database connection [upon creation, this is a local variable, otherwise the class variable] * @param mbtiles_metadata list of key,values to update. [fill this with valued that need to be added/changed] * @param i_reload_metadata 1: reload values after update [not needed upon creation, update after bounds/center/zoom changes] * @return 0: no error */ public int update_mbtiles_metadata(SQLiteDatabase mbtiles_db, HashMap<String, String> mbtiles_metadata, int i_reload_metadata) throws IOException { // i_rc=0: no error int i_rc = 0; if (mbtiles_db == null) mbtiles_db = getmbtiles(); if (mbtiles_metadata == null) mbtiles_metadata = this.mbtiles_metadata; String s_metadata_tablename = "metadata"; String s_tile_insert_metadata = ""; // ---------------------------------------------- db_lock.writeLock().lock(); mbtiles_db.beginTransaction(); try { for (Map.Entry<String, String> metadata : mbtiles_metadata.entrySet()) { s_tile_insert_metadata = "INSERT OR REPLACE INTO '" + s_metadata_tablename + "' VALUES('" + metadata.getKey() + "','" + metadata.getValue() + "')"; mbtiles_db.execSQL(s_tile_insert_metadata); // GPLog.androidLog(-1, "MBTilesDroidSpitter:update_mbtiles_metadata[" + // s_tile_insert_metadata + "]"); } mbtiles_db.setTransactionSuccessful(); } catch (Exception e) { i_rc = 1; throw new IOException("MBTilesDroidSpitter:update_mbtiles_metadata sql[" + s_tile_insert_metadata + "] error[" + e.getLocalizedMessage() + "]"); } finally { mbtiles_db.endTransaction(); db_lock.writeLock().unlock(); if (i_reload_metadata == 1) { // should be done when bounds of min/max zoom has changed try { fetchMetadata(this.s_metadataVersion); } catch (Exception e) { GPLog.error(this, null, e); i_rc = 1; } } } // ---------------------------------------------- return i_rc; } // ----------------------------------------------- /** * Retrieve min/max tiles for each zoom-level from mbtiles * - no checking for possible 'holes' inside zoom-level are done * * @param i_reload_metadata reload values after update [not needed upon creation, update after bounds/center/zoom changes] * @param i_update update to mbiles database if bounds min/max zoom have changed * @return the retrieved values. ['zoom','min_x,min_y,max_x,max_y'] */ public HashMap<String, String> fetch_bounds_minmax(int i_reload_metadata, int i_update) { HashMap<String, String> update_metadata = new LinkedHashMap<String, String>(); HashMap<String, String> bounds_min_max = fetch_bounds_minmax_tiles(); String s_zoom_min_max = ""; String s_bounds_tiles = ""; String s_bounds_center = ""; String s_minzoom = ""; String s_maxzoom = ""; String s_centerzoom = ""; // GPLog.androidLog(-1,"MBTilesDroidSpitter.fetch_bounds_minmax: parms["+i_reload_metadata+"/"+i_update+"] bounds_min_max=["+bounds_min_max.size()+"]"); if (bounds_min_max.size() > 0) { if (bounds_lat_long != null) { bounds_lat_long.clear(); } bounds_lat_long = fetch_bounds_minmax_latlong(bounds_min_max, 256); if (bounds_lat_long.size() > 0) { // how to retrieve that last value only? for (Map.Entry<String, String> zoom_levels : bounds_lat_long.entrySet()) { s_zoom_min_max = zoom_levels.getKey(); s_bounds_tiles = zoom_levels.getValue(); } if ((s_bounds_tiles != "") && (s_bounds_tiles != "")) { String[] sa_splitted = s_zoom_min_max.split(","); if (sa_splitted.length == 3) { // only the last record (with min/max/center // zoom) will // be used s_minzoom = sa_splitted[0]; s_maxzoom = sa_splitted[1]; s_centerzoom = sa_splitted[2]; if ((s_minzoom != "") && (s_maxzoom != "")) { sa_splitted = s_bounds_tiles.split(";"); if (sa_splitted.length == 2) { s_bounds_tiles = sa_splitted[0]; s_bounds_center = sa_splitted[1] + "," + s_centerzoom; sa_splitted = s_bounds_tiles.split(","); if (sa_splitted.length == 4) { update_metadata.put("bounds", s_bounds_tiles); update_metadata.put("minzoom", s_minzoom); update_metadata.put("maxzoom", s_maxzoom); update_metadata.put("center", s_bounds_center); } } } } } } } if ((i_update == 1) && (update_metadata.size() > 0)) { // GPLog.androidLog(1,"fetch_bounds_minmax[update] bounds["+s_bounds_tiles+"] minzoom["+s_minzoom+"] maxzoom["+s_maxzoom+"] center["+s_bounds_center+"]"); try { update_mbtiles_metadata(db_mbtiles, update_metadata, i_reload_metadata); } catch (Exception e) { GPLog.error(this, "MBTilesDroidSpitter[" + file_mbtiles.getAbsolutePath() + "]", e); } } return update_metadata; } // ----------------------------------------------- // SELECT count(tile_id) from map WHERE zoom_level = 7; // SELECT tile_id from map WHERE zoom_level = 7; // to delete a zoom_level properly: // DELETE FROM map WHERE zoom_level = 7; // DELETE FROM map WHERE tile_id = "ff-ff-ff.rgb"; // ----------------------------------------------- /** * Retrieve min/max tiles for each zoom-level from mbtiles * - no checking for possible 'holes' inside zoom-level are done * - 20131107 mj10777: this function causes problems when online retrieving is done * -- with big databases it takes time to compleate this, so the application can stall or crash * --- an alternitive will be worked out at a later time * - 20131123: now work correctl and speedaly - but should only be used when really needed * * @return the retrieved values. ['zoom','min_x,min_y,max_x,max_y'] */ public HashMap<String, String> fetch_bounds_minmax_tiles() { HashMap<String, String> bounds_min_max = new LinkedHashMap<String, String>(); List<Integer> zoom_levels = null; int i_zoom_level = 0; int i_version = 0; // These querys run much quicker with 'maps" String s_table = "map"; if (i_type_tiles == 0) { // mbtiles is only valid if 'i_type_tiles' == 0 or 1 [table or // view] s_table = "tiles"; // map will not exist } // SELECT zoom_level,min(tile_column) AS min_x,min(tile_row) AS min_y,max(tile_column) AS // max_x,max(tile_row) AS max_y FROM tiles WHERE zoom_level IN(SELECT DISTINCT zoom_level // FROM tiles ORDER BY zoom_level ASC) GROUP BY zoom_level // tiles: 00:00:07.160 ; map 00:00:03.200 // SELECT DISTINCT zoom_level FROM tiles ORDER BY zoom_level ASC // tiles: 00:00:03.209 ; map 00:00:00.040 // SELECT zoom_level,min(tile_column) AS min_x,min(tile_row) AS min_y,max(tile_column) AS // max_x,max(tile_row) AS max_y FROM tiles WHERE zoom_level = 16 // tiles: 00:00:03.200 ; map 00:00:00.040 String SQL_GET_MINMAXZOOM_TILES = "SELECT zoom_level,min(tile_column) AS min_x,min(tile_row) AS min_y,max(tile_column) AS max_x,max(tile_row) AS max_y FROM " + s_table + " WHERE zoom_level IN(SELECT DISTINCT zoom_level FROM " + s_table + " ORDER BY zoom_level ASC) GROUP BY zoom_level"; Cursor c_tiles = null; db_lock.readLock().lock(); try { // GPLog.androidLog(-1,"MBTilesDroidSpitter.fetch_bounds_minmax_tiles: sql["+SQL_GET_MINMAXZOOM_TILES+"]"); c_tiles = db_mbtiles.rawQuery(SQL_GET_MINMAXZOOM_TILES, null); // mj10777: 20131123: avoid using table/view 'tiles' - with big databases can bring // things to a halt if ((c_tiles != null) && (c_tiles.getColumnCount() > 0) && (c_tiles.getCount() > 0)) { // avoid // CursorIndexOutOfBoundsException c_tiles.moveToFirst(); do { // 12 2197 2750 2203 2754 String s_zoom = c_tiles.getString(0); String s_bounds_tiles = c_tiles.getString(1) + "," + c_tiles.getString(2) + "," + c_tiles.getString(3) + "," + c_tiles.getString(4); bounds_min_max.put(s_zoom, s_bounds_tiles); // GPLog.androidLog(-1,"MBTilesDroidSpitter.fetch_bounds_minmax_tiles: sql["+s_zoom+","+s_bounds_tiles+"]"); } while (c_tiles.moveToNext()); c_tiles.close(); } } catch (Exception e) { GPLog.error(this, "MBTilesDroidSpitter.fetch_bounds_minmax_tiles: sql[" + SQL_GET_MINMAXZOOM_TILES + "]", e); } finally { db_lock.readLock().unlock(); } return bounds_min_max; } // ----------------------------------------------- /** * Convert zoom/min/max tile number bounds into lat long for each zoom-level * - last entry the min-max zoom and the min/max lat/long of all zoom-levels * * @param bounds_tiles (result of fetch_bounds_minmax_tiles()) * @return the converted values. ['zoom','min_x,min_y,max_x,max_y'] */ public HashMap<String, String> fetch_bounds_minmax_latlong(HashMap<String, String> bounds_tiles, int i_tize_size) { double[] max_bounds = new double[]{180.0, 85.05113, -180.0, -85.05113, 0, 0}; int i_min_zoom = 22; int i_max_zoom = 0; int i_zoom_center = 0; HashMap<String, String> bounds_lat_long = new LinkedHashMap<String, String>(); for (Map.Entry<String, String> zoom_bounds : bounds_tiles.entrySet()) { double[] tile_bounds = tile_bounds_to_latlong(zoom_bounds.getKey(), zoom_bounds.getValue(), i_tize_size); int i_zoom = Integer.parseInt(zoom_bounds.getKey()); if (i_zoom < i_min_zoom) i_min_zoom = i_zoom; if (i_zoom > i_max_zoom) i_max_zoom = i_zoom; if (tile_bounds[0] < max_bounds[0]) max_bounds[0] = tile_bounds[0]; if (tile_bounds[1] < max_bounds[1]) max_bounds[1] = tile_bounds[1]; if (tile_bounds[2] > max_bounds[2]) max_bounds[2] = tile_bounds[2]; if (tile_bounds[3] > max_bounds[3]) max_bounds[3] = tile_bounds[3]; if (((tile_bounds[4] >= max_bounds[0]) && (tile_bounds[4] <= max_bounds[2])) && ((tile_bounds[5] >= max_bounds[1]) && (tile_bounds[5] <= max_bounds[3]))) { if (i_zoom_center < i_zoom) { // The center of the highest Zoom-Level will be set as // center max_bounds[4] = tile_bounds[4]; max_bounds[5] = tile_bounds[5]; i_zoom_center = i_zoom; } } String s_bounds_tiles = tile_bounds[0] + "," + tile_bounds[1] + "," + tile_bounds[2] + "," + tile_bounds[3] + ";" + tile_bounds[4] + "," + tile_bounds[5]; bounds_lat_long.put(zoom_bounds.getKey(), s_bounds_tiles); // GPLog.androidLog(-1,"MBTilesDroidSpitter.fetch_bounds_minmax_tiles: bounds_lat_long["+i_zoom+","+s_bounds_tiles+"]"); } String s_zoom = i_min_zoom + "," + i_max_zoom + "," + i_zoom_center; String s_bounds_tiles = max_bounds[0] + "," + max_bounds[1] + "," + max_bounds[2] + "," + max_bounds[3] + ";" + max_bounds[4] + "," + max_bounds[5]; bounds_lat_long.put(s_zoom, s_bounds_tiles); return bounds_lat_long; } // ----------------------------------------------- /** * Retrieve zoom_level, x_tile,y_tile_osm from tile_id * - y_tile_osm is always in osm notation * -- 'tms' or 'osm' depending on the setting of the active mbtiles.db * * @param i_z zoom_level * @param i_x x_tile * @param i_y_osm y_tile [n osm notation] * @return s_tile_id tile_id to use [z-x-y.osm/tms] */ public String get_tile_id_from_zxy(int i_z, int i_x, int i_y_osm) { int i_y = i_y_osm; if (s_tile_row_type.equals("tms")) { int[] tmsTileXY = MBTilesDroidSpitter.googleTile2TmsTile(i_x, i_y_osm, i_z); i_y = tmsTileXY[1]; } return i_z + "-" + i_x + "-" + i_y + "." + s_tile_row_type; // 'tms' or 'osm'; } // ----------------------------------------------- /** * Converts min/max tile-numbers of zoom level into mix/max lat/long * * @param s_zoom the zoom level (as string). * @param s_tile_bounds format ['min_x,min_y_tms,max_x,max_y_tms'] in tile-numbers of zoom * @param i_tize_size tile size [256]. * @return the converted values. ['min_x,min_y,max_x,max_y'] */ public static double[] tile_bounds_to_latlong(String s_zoom, String s_tile_bounds, int i_tize_size) { // min_x,min_y_tms,max_x,max_y_tms String[] sa_splitted = s_tile_bounds.split(","); if (sa_splitted.length != 4) return null; int[] tile_bounds = new int[4]; int i_zoom = 0; try { for (int i = 0; i < sa_splitted.length; i++) { if (i == 0) i_zoom = Integer.parseInt(s_zoom); tile_bounds[i] = Integer.parseInt(sa_splitted[i]); } } catch (NumberFormatException e) { return null; } int i_min_x = tile_bounds[0]; int i_min_y_tms = tile_bounds[1]; int i_max_x = tile_bounds[2]; int i_max_y_tms = tile_bounds[3]; int[] tms_values = tmsTile2GoogleTile(i_min_x, i_min_y_tms, i_zoom); int i_min_y_osm = tms_values[1]; tms_values = tmsTile2GoogleTile(i_max_x, i_max_y_tms, i_zoom); int i_max_y_osm = tms_values[1]; double[] bounds = tileLatLonBounds(i_min_x, i_min_y_osm, i_zoom, i_tize_size); double d_min_x = bounds[0]; double d_min_y = bounds[1]; bounds = tileLatLonBounds(i_max_x, i_max_y_osm, i_zoom, i_tize_size); double d_max_x = bounds[2]; double d_max_y = bounds[3]; double d_center_x = (d_max_x + d_min_x) / 2; double d_center_y = (d_max_y + d_min_y) / 2; return new double[]{d_min_x, d_min_y, d_max_x, d_max_y, d_center_x, d_center_y}; } // ----------------------------------------------- /** * Converts byte[] to hex String * * @param ba_data the byte[] to convert * @return the hex string to be used to compare with sql 'WHERE (hex(tile_data) = '?')' */ public static String get_hex(byte[] ba_data) { if (ba_data == null) { return null; } final StringBuilder sb_hex = new StringBuilder(2 * ba_data.length); Formatter formatter = new Formatter(sb_hex); for (final byte b : ba_data) { formatter.format("%02x", b); } return sb_hex.toString(); } // ----------------------------------------------- static int[] rb_table = null; // for get_pixel_rgb static int[] g_table = null; // get_pixel_rgb // ----------------------------------------------- /** * Determin if the Bitmap is blank (i.e. all pixels are of ONE colour) * - RGB_565 will be converted to ARGB_8888 * * @param this_bitmap Bitmap to check * @param i_parm 0: 'ffeedd' ; 1: 'ff-ee-dd.rgb' [to be used as tile_id] * @return RGB Values of this colour if unique, otherwise empty */ public static String get_pixel_rgb_toString(Bitmap this_bitmap, int i_parm) { String s_rgb = ""; // ---------------------------------------------- int[] rgb = get_pixel_rgb(this_bitmap); if ((rgb != null) && (rgb.length == 3)) { for (int i = 0; i < rgb.length; i++) { String s_rgb_value = String.format("%02x", (0xFF & rgb[i])); // .toUpperCase(); if ((i_parm == 1) && (i < (rgb.length - 1))) s_rgb_value = s_rgb_value + "-"; s_rgb = s_rgb + s_rgb_value; } if (i_parm == 1) s_rgb = s_rgb + ".rgb"; } // ---------------------------------------------- return s_rgb; } // ----------------------------------------------- /** * Retrieve zoom_level, x_tile,y_tile_osm from tile_id * - y_tile_osm is always in osm notation * * @param s_tile_id tile_id to use * @return zxy_osm z,x,y_osm values */ public static int[] get_zxy_from_tile_id(String s_tile_id) { int[] zxy_osm = null; String[] sa_string = s_tile_id.split("-"); if (sa_string.length == 3) { // s_tile_id = i_z + "-" + i_x + "-" + i_y + "." + // s_tile_row_type; // 'tms' or 'osm' int i_z = Integer.parseInt(sa_string[0]); int i_x = Integer.parseInt(sa_string[1]); String s_y = sa_string[2]; sa_string = s_y.split("\\."); // not .split("."); // get_zxy_from_tile_id[17-70427-88057.tms] s_y[88057.tms] [0] // GPLog.androidLog(-1,"get_zxy_from_tile_id["+s_tile_id+"] s_y["+s_y+"] ["+sa_string.length+"]"); if (sa_string.length == 2) { int i_y_osm = Integer.parseInt(sa_string[0]); int i_y_tms = i_y_osm; s_y = sa_string[1]; zxy_osm = new int[]{i_z, i_x, i_y_osm, i_y_tms}; if (s_y.equals("tms")) { int[] xy_osm = tmsTile2GoogleTile(i_x, i_y_tms, i_z); if (xy_osm.length == 2) { zxy_osm[2] = xy_osm[1]; } } } } return zxy_osm; } // ----------------------------------------------- /** * Determin if the Bitmap is blank (i.e. all pixels are of ONE colour) * - RGB_565 will be converted to ARGB_8888 * * @param this_bitmap Bitmap to check * @return RGB Values of this colour, otherwise null */ public static int[] get_pixel_rgb(Bitmap this_bitmap) { int[] rgb = null; int i_image_width = this_bitmap.getWidth(); int i_image_height = this_bitmap.getHeight(); Bitmap.Config i_bitmap_config = this_bitmap.getConfig(); int i_R = 0; int i_G = 0; int i_B = 0; // ---------------------------------------------- for (int x = 0; x < i_image_width; x++) { for (int y = 0; y < i_image_height; y++) { int[] pixel_rgb = get_pixel_rgb(i_bitmap_config, this_bitmap.getPixel(x, y)); if ((pixel_rgb != null) && (pixel_rgb[0] != 0) && (pixel_rgb[1] != 0) && (pixel_rgb[2] != 0)) { if ((x == 0) && (y == 0)) { i_R = pixel_rgb[0]; i_G = pixel_rgb[1]; i_B = pixel_rgb[2]; } else { if ((pixel_rgb[0] != i_R) || (pixel_rgb[1] != i_G) || (pixel_rgb[2] != i_B)) { return rgb; // This image has more than one color, return null } } } } } // ---------------------------------------------- rgb = new int[]{i_R, i_G, i_B}; // This image is of one color, return the RGB Values // ---------------------------------------------- return rgb; } // ----------------------------------------------- /** * Retrieve RGB value of Bitmap Pixel * - RGB_565 will be converted to ARGB_8888 * * @param i_bitmap_config Bitmap.Config of Bitmap * @param i_pixel Bitmap.Pixel value * @return RGB Value of this Pixel, otherwise null */ public static int[] get_pixel_rgb(Bitmap.Config i_bitmap_config, int i_pixel) { // i_image_height // - i_pixel = // 0xff000000 // | (rgb[0] // << 16) | // (rgb[1] << // 8) | // rgb[2]; int[] rgb = null; switch (i_bitmap_config) { case RGB_565: { // Bitmap.Config.RGB_565=4 if (rb_table == null) { int i = 32; rb_table = new int[i]; for (i = 0; i < 32; i++) rb_table[i] = 255 * i / 31; i = 64; g_table = new int[i]; for (i = 0; i < 64; i++) g_table[i] = 255 * i / 63; } rgb = new int[]{rb_table[(i_pixel >> 11) & 31] << 16, g_table[(i_pixel >> 5) & 63] << 8, rb_table[i_pixel & 31]}; } break; // ALPHA_8, ARGB_4444 case ARGB_8888: { rgb = new int[]{(i_pixel & 0xff0000) >> 16, (i_pixel & 0x00ff00) >> 8, (i_pixel & 0x0000ff) >> 0}; // ARGB_8888 - i_pixel = 0xff000000 | (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; } break; } return rgb; // return new int[]{i_pixel >> 16 & 0xff,i_pixel >> 8 & 0xff,i_pixel >> 0xff}; } // ----------------------------------------------- // mj10777: copied from // - geopaparazzilibrary/src/eu/geopaparazzi/library/util/Utilities.java // ----------------------------------------------- static double originShift = 2 * Math.PI * 6378137 / 2.0; /** * Converts Google tile coordinates to TMS Tile coordinates. * <p/> * <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p> * * @param tx the x tile number. * @param ty the y tile number. [osm notation] * @param zoom the current zoom level. * @return the converted values. */ public static int[] googleTile2TmsTile(int tx, int ty, int zoom) { return new int[]{tx, (int) ((Math.pow(2, zoom) - 1) - ty)}; } /** * Converts TMS tile coordinates to Google Tile coordinates. * <p/> * <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p> * * @param tx the x tile number. * @param ty the y tile number. [tms notation] * @param zoom the current zoom level. * @return the converted values. */ public static int[] tmsTile2GoogleTile(int tx, int ty, int zoom) { return new int[]{tx, (int) ((Math.pow(2, zoom) - 1) - ty)}; } /** * <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p> * * @param tx * @param ty [osm notation] * @param zoom * @param tileSize * @return [minx, miny, maxx, maxy] */ public static double[] tileLatLonBounds(int tx, int ty, int zoom, int tileSize) { double[] bounds = tileBounds(tx, ty, zoom, tileSize); double[] mins = metersToLatLon(bounds[0], bounds[1]); double[] maxs = metersToLatLon(bounds[2], bounds[3]); return new double[]{mins[1], maxs[0], maxs[1], mins[0]}; } /** * <p>Code copied from: http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers </p> * 20131128: corrections added to correct going over or under max/min extent * - was causing http 400 Bad Requests * - updated openstreetmap wiki * * @param zoom * @return [zoom, xtile, ytile_osm] */ public static int[] getTileNumber(final double lat, final double lon, final int zoom) { int xtile = (int) Math.floor((lon + 180) / 360 * (1 << zoom)); int ytile_osm = (int) Math.floor((1 - Math.log(Math.tan(Math.toRadians(lat)) + 1 / Math.cos(Math.toRadians(lat))) / Math.PI) / 2 * (1 << zoom)); if (xtile < 0) xtile = 0; if (xtile >= (1 << zoom)) xtile = ((1 << zoom) - 1); if (ytile_osm < 0) ytile_osm = 0; if (ytile_osm >= (1 << zoom)) ytile_osm = ((1 << zoom) - 1); return new int[]{zoom, xtile, ytile_osm}; } /** * <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p> * * @param latlong_bounds [minx,miny,maxx,minx] * @param i_zoom * @return [zoom, minx, miny, maxx, maxy of tile_bounds] */ public static int[] LatLonBounds_to_TileBounds(double[] latlong_bounds, int i_zoom) { int[] min_tile_bounds = getTileNumber(latlong_bounds[1], latlong_bounds[0], i_zoom); int[] max_tile_bounds = getTileNumber(latlong_bounds[3], latlong_bounds[2], i_zoom); return new int[]{i_zoom, min_tile_bounds[1], min_tile_bounds[2], max_tile_bounds[1], max_tile_bounds[2]}; } /** * <p>Code adapted from: LatLonBounds_to_TileBounds</p> * * @param tile_bounds [minx, miny_osm, maxx, maxy_osm of tile_bounds] * @param i_zoom * @return latlong_bounds [minx,miny,maxx,minx] */ public static double[] TileBounds_to_LatLonBounds(int[] tile_bounds, int i_zoom) { int i_min_x = tile_bounds[0]; int i_min_y_osm = tile_bounds[1]; int i_max_x = tile_bounds[2]; int i_max_y_osm = tile_bounds[3]; double[] bounds = tileLatLonBounds(i_min_x, i_min_y_osm, i_zoom, 256); double d_min_x = bounds[0]; double d_min_y = bounds[1]; bounds = tileLatLonBounds(i_max_x, i_max_y_osm, i_zoom, 256); double d_max_x = bounds[2]; double d_max_y = bounds[3]; return new double[]{d_min_x, d_min_y, d_max_x, d_max_y}; } /** * Returns bounds of the given tile in EPSG:900913 coordinates * <p/> * <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p> * * @param tx * @param ty * @param zoom * @return [minx, miny, maxx, maxy] */ public static double[] tileBounds(int tx, int ty, int zoom, int tileSize) { double[] min = pixelsToMeters(tx * tileSize, ty * tileSize, zoom, tileSize); double minx = min[0], miny = min[1]; double[] max = pixelsToMeters((tx + 1) * tileSize, (ty + 1) * tileSize, zoom, tileSize); double maxx = max[0], maxy = max[1]; return new double[]{minx, miny, maxx, maxy}; } /** * Converts pixel coordinates in given zoom level of pyramid to EPSG:900913 * <p/> * <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p> * * @return */ public static double[] pixelsToMeters(double px, double py, int zoom, int tileSize) { double res = getResolution(zoom, tileSize); double mx = px * res - originShift; double my = py * res - originShift; return new double[]{mx, my}; } /** * Converts XY point from Spherical Mercator EPSG:900913 to lat/lon in WGS84 * Datum * <p/> * <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p> * * @return */ public static double[] metersToLatLon(double mx, double my) { // double originShift = 2 * // Math.PI * 6378137 / 2.0; double lon = (mx / originShift) * 180.0; double lat = (my / originShift) * 180.0; lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180.0)) - Math.PI / 2.0); return new double[]{-lat, lon}; } /** * Resolution (meters/pixel) for given zoom level (measured at Equator) * <p/> * <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p> * * @return */ public static double getResolution(int zoom, int tileSize) { // return (2 * Math.PI * 6378137) / (this.tileSize * 2**zoom) double initialResolution = 2 * Math.PI * 6378137 / tileSize; return initialResolution / Math.pow(2, zoom); } }