/** @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.database.Cursor; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.TreeMap; public class MbTilesMetadata { public final String name, description, type, version, format; public final float[] bounds; public final float[] center; public final int minZoom; public final int maxZoom; public final int defaultZoom; public String s_tile_row_type = "tms"; public String s_center_parm = ""; public final Map<String, String> extra; public static final MetadataValidatorFactory metadataValidatorFactory = new MetadataValidatorFactory(); // ----------------------------------------------- /** * Constructor MbTilesMetadata * * <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 name The name of the tileset. * @param description A description of the layer as plain text. * @param version The version of the tileset, as a plain number. * @param format The image file format of the tile data: png or jpg * @param bounds Should be latitude and longitude values in OpenLayers Bounds format - left, bottom, right, top. * @param center A default position and Zoom that can be set by the MBTiles designer * @param minZoom minimum Zoom level * @param maxZoom maximum Zoom level * @param s_tile_row_type how the y tile-position is to be interpreted ['tms' or 'osm'] * @param extra any other values found in the metadata table */ public MbTilesMetadata( String name, String description, String type, String version, String format, float[] bounds, float[] center, int minZoom, int maxZoom, String s_tile_row_type, Map<String, String> extra ) { this.name = name; this.type = type; this.version = version; this.description = description; this.format = format; if (bounds == null) { // -180.0,-85,180,85 bounds = new float[]{-180.0f, -85.05113f, 180.0f, 85.05113f}; } this.bounds = bounds; this.minZoom = minZoom; this.maxZoom = maxZoom; this.extra = extra; if (center != null) { if ((center[0] < this.bounds[0]) || (center[0] > this.bounds[2])) { center[0] = this.bounds[0] + (this.bounds[2] - this.bounds[0]) / 2f; } if ((center[1] < this.bounds[1]) || (center[1] > this.bounds[3])) { center[1] = this.bounds[1] + (this.bounds[3] - this.bounds[1]) / 2f; } center[2] = (int) center[2]; if ((center[2] < minZoom) || (center[2] > maxZoom)) center[2] = minZoom; // .map files only have a minZoom, this will be used so that // everything reacte in the same way this.center = center; } else { this.center = new float[]{this.bounds[0] + (this.bounds[2] - this.bounds[0]) / 2f, this.bounds[1] + (this.bounds[3] - this.bounds[1]) / 2f, maxZoom}; } if ((s_tile_row_type != "") && ((s_tile_row_type.equals("tms")) || (s_tile_row_type.equals("osm")))) this.s_tile_row_type = s_tile_row_type; this.defaultZoom = (int) this.center[2]; this.s_center_parm = center[0] + "," + center[1] + "," + this.defaultZoom; } @Override public String toString() { String none = " - "; String separator = " | "; StringBuilder sb = new StringBuilder("Metadata:\n"); sb.append("Name: " + (name == null ? none : name) + separator); sb.append("Type: " + (type == null ? none : type) + separator); sb.append("Version: " + (version == null ? none : version) + separator); sb.append("Description: " + (description == null ? none : description) + separator); sb.append("Format: " + (format == null ? none : format) + separator); sb.append("Bounds: " + (bounds == null ? none : bounds)); sb.append("Zoom [min]: " + minZoom); sb.append("Zoom [max]: " + maxZoom); sb.append("Center: " + (center == null ? none : center)); sb.append("Zoom [default]: " + defaultZoom); sb.append("tile_row_type: " + (s_tile_row_type == null ? none : s_tile_row_type)); sb.append("\n\n"); sb.append("Extra: " + this.extra.toString() + ""); return sb.toString(); } // ----------------------------------------------- /** * createFromCursor * @param c: Sql Cursor being used * @param idx_col_key: index field of the metdata field 'name' [should be 0] * @param idx_col_value: index field of the metdata field 'value' [should be 1] * @return HashMap<String,String> with key,values to be validated */ public static MbTilesMetadata createFromCursor( Cursor c, int idx_col_key, int idx_col_value, MetadataValidator validator ) throws MetadataParseException { Map<String, String> dumped = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER); c.moveToFirst(); do { dumped.put(c.getString(idx_col_key), c.getString(idx_col_value)); } while( c.moveToNext() ); c.close(); return validator.validate(dumped); } public static interface MetadataValidator { MbTilesMetadata validate( Map<String, String> hm ) throws MetadataParseException; } @SuppressWarnings("serial") public static class MetadataParseException extends Exception { public MetadataParseException( String errorMessage ) { super(errorMessage); } } public static class MetadataValidatorFactory { /* Validators for parsing Metadata */ private final static MetadataValidator VALIDATOR_1_0 = new MetadataValidator_1_0(); private final static MetadataValidator VALIDATOR_1_1 = new MetadataValidator_1_1(); public static MetadataValidator getMetadataValidatorFromVersion( String version ) { if (version.equals("1.0")) return VALIDATOR_1_0; if (version.equals("1.1")) return VALIDATOR_1_1; return null; } } // ----------------------------------------------- /** * validate MetadataValidator_1_1 * * <p>https://github.com/mapbox/MbTiles.spec/blob/master/1.1/spec.md </br> * mj10777: since this is a reader, we will attempt to supply default * values for missing or incorrect values</p> * * <ul> * <li>at the moment, only 1.1 should be called</li> * <li>checking of mandatory fields should exist (to insure that this is really a mbtiles file)</li> * </ul> * * @param name: [mandatory] The name of the tileset. * @param description: [mandatory] A description of the layer as plain text. * @param version: [mandatory] The version of the tileset, as a plain number. * @param format: [mandatory] The image file format of the tile data: png or jpg * @param bounds: [optional] Should be latitude and longitude values in OpenLayers Bounds format - left, bottom, right, top. * @param center: [tilemill specific,unofficial] A default position and Zoom that can be set by the MBTiles designer * @param minZoom: [optional] minimum Zoom level * @param maxZoom: [optional] maximum Zoom level * @param s_tile_row_type: [suggested,unofficial] how the y tile-position is to be interpreted ['tms' or 'osm'] * @param extra any other values found in the metadata table */ public static class MetadataValidator_1_1 implements MetadataValidator { @Override public MbTilesMetadata validate( Map<String, String> hm ) throws MetadataParseException { String tmp; String name = hm.remove("name"); if (name == null) throw new MetadataParseException("No mandatory field 'name'."); String description = hm.remove("description"); if (description == null) { // gdal does not fill this value [it can have empty (NULL) // value] description = name; // throw new MetadataParseException("No mandatory field 'description'."); } String type = hm.remove("type"); if (type == null || (!type.equals("overlay") && type.equals("baselayer"))) { // we suppose it is a baselayer by default, if not available type = "baselayer"; } String version = "1.1"; tmp = hm.remove("version"); if (tmp == null) throw new MetadataParseException("No mandatory field 'version'"); String format = hm.remove("format"); if (format == null || (!format.equals("png") && !format.equals("jpg"))) { // This application does NOT need to know the image type to display format = "jpg"; } // bounds: optional // - should be set to zoom to this area when first loading the map, if the present point // is out of range // -- user friendly, otherwise the user may be lost in a sea of white tiles. tmp = hm.remove("bounds"); float[] bounds = {-180.0f, -85.05113f, 180.0f, 85.05113f}; if (tmp != null) { bounds = ValidatorHelper.parseBounds(tmp); if (bounds == null) { bounds = new float[]{-180.0f, -85.05113f, 180.0f, 85.05113f}; // throw new // MetadataParseException("Invalid syntax for optional field 'bounds'." // + // "Should be latitude and longitude values in OpenLayers Bounds format - left, bottom, right, top." // + "Example of the full earth: -180.0,-85,180,85"); } } String minZoomStr = hm.remove("minzoom"); int minZoom = 0; if (minZoomStr != null) { minZoom = Integer.parseInt(minZoomStr); } String maxZoomStr = hm.remove("maxzoom"); int maxZoom = 22; if (maxZoomStr != null) { maxZoom = Integer.parseInt(maxZoomStr); } if (minZoom > maxZoom) { int i_zoom = minZoom; minZoom = maxZoom; maxZoom = i_zoom; } if ((minZoom < 0) || (minZoom > 22)) minZoom = 0; if ((maxZoom < 0) || (maxZoom > 22)) maxZoom = 22; // center: tilemill specific parameter // - not part of the specification, but usefull when first loading and the map is out of // the range // - the map designer can determin the 'main point of interest' and desired zoom level // -- which may NOT be the center of the map OR the minZoom [which are the default] // - the application will only use this point when the present point is outside the // bounds tmp = hm.remove("center"); float[] center = {bounds[0] + (bounds[2] - bounds[0]) / 2f, bounds[1] + (bounds[3] - bounds[1]) / 2f, maxZoom}; if (tmp != null) { center = ValidatorHelper.parseCenter(tmp); if (center == null) { center = new float[]{bounds[0] + (bounds[2] - bounds[0]) / 2f, bounds[1] + (bounds[3] - bounds[1]) / 2f, maxZoom}; // throw new // MetadataParseException("Invalid syntax for optional field 'center'." // + // "Should be latitude and longitude values in as - center_x, center_y, rzoom." // + "Example of the full earth: 0,0,1"); } } // tile_row_type: possible support for non-tms numbering // - this value has been suggested, but not acepted and should be used with care String s_tile_row_type = hm.remove("tile_row_type"); if (s_tile_row_type == null || (!s_tile_row_type.equals("tms") && !s_tile_row_type.equals("osm"))) { // Until this is accepted, other application will get 'confused' // when the non-tms numbering is used s_tile_row_type = "tms"; } return new MbTilesMetadata(name, description, type, version, format, bounds, center, minZoom, maxZoom, s_tile_row_type, hm); } } // ----------------------------------------------- /** * validate MetadataValidator_1_0 * * <p>https://github.com/mapbox/MbTiles.spec/blob/master/1.0/spec.md </br> * mj10777: since this is a reader, we will attempt to supply default * values for missing or incorrect values</p> * * <ul> * <li>at the moment, only 1.1 should be called</li> * <li>checking of mandatory fields should exist (to insure that this is really a mbtiles file)</li> * </ul> * * @param name: [mandatory] The name of the tileset. * @param description: [mandatory] A description of the layer as plain text. * @param version: [mandatory] The version of the tileset, as a plain number. * @param format: [mandatory] The image file format of the tile data: png or jpg * @param bounds: [optional] Should be latitude and longitude values in OpenLayers Bounds format - left, bottom, right, top. * @param center: [tilemill specific,unofficial] A default position and Zoom that can be set by the MBTiles designer * @param minZoom: [optional] minimum Zoom level * @param maxZoom: [optional] maximum Zoom level * @param s_tile_row_type: [suggested,unofficial] how the y tile-position is to be interpreted ['tms' or 'osm'] * @param extra any other values found in the metadata table */ public static class MetadataValidator_1_0 implements MetadataValidator { @Override public MbTilesMetadata validate( Map<String, String> hm ) throws MetadataParseException { String tmp; String name = hm.remove("name"); if (name == null) throw new MetadataParseException("No mandatory field 'name'."); String description = hm.remove("description"); if (description == null) throw new MetadataParseException("No mandatory field 'description'."); String type = hm.remove("type"); if (type == null || (!type.equals("overlay") && type.equals("baselayer"))) { // we suppose it is a baselayer by default, if not available type = "baselayer"; // throw new // MetadataParseException("No mandatory field 'type' or not in [ overlay, baselayer ]."); } String version = "1.0"; tmp = hm.remove("version"); if (tmp == null) throw new MetadataParseException("No mandatory field 'version'"); String format = hm.remove("format"); if (format == null || (!format.equals("png") && !format.equals("jpg"))) { // This application does NOT need to know the image type to display format = "jpg"; } // bounds: optional // - should be set to zoom to this area when first loading the map, if the present point // is out of range // -- user friendly, otherwise the user may be lost in a sea of white tiles. tmp = hm.remove("bounds"); float[] bounds = {-180.0f, -85.05113f, 180.0f, 85.05113f}; if (tmp != null) { // some tilemill db use this - despite the 1.0.0 version number bounds = ValidatorHelper.parseBounds(tmp); if (bounds == null) { bounds = new float[]{-180.0f, -85.05113f, 180.0f, 85.05113f}; // throw new // MetadataParseException("Invalid syntax for optional field 'bounds'." // + // "Should be latitude and longitude values in OpenLayers Bounds format - left, bottom, right, top." // + "Example of the full earth: -180.0,-85,180,85"); } } String minZoomStr = hm.remove("minzoom"); int minZoom = 0; if (minZoomStr != null) { minZoom = Integer.parseInt(minZoomStr); } String maxZoomStr = hm.remove("maxzoom"); int maxZoom = 22; if (maxZoomStr != null) { maxZoom = Integer.parseInt(maxZoomStr); } // center: tilemill specific parameter // - not part of the specification, but usefull when first loading and the map is out of // the range // - the map designer can determin the 'main point of interest' and desired zoom level // -- which may NOT be the center of the map OR the minZoom [which are the default] // - the application will only use this point when the present point is outside the // bounds tmp = hm.remove("center"); float[] center = {bounds[0] + (bounds[2] - bounds[0]) / 2f, bounds[1] + (bounds[3] - bounds[1]) / 2f, maxZoom}; if (tmp != null) { center = ValidatorHelper.parseCenter(tmp); if (center == null) { center = new float[]{bounds[0] + (bounds[2] - bounds[0]) / 2f, bounds[1] + (bounds[3] - bounds[1]) / 2f, maxZoom}; // throw new // MetadataParseException("Invalid syntax for optional field 'center'." // + // "Should be latitude and longitude values in as - center_x, center_y, rzoom." // + "Example of the full earth: 0,0,1"); } } // tile_row_type: possible support for non-tms numbering // - this value has been suggested, but not acepted and should be used with care String s_tile_row_type = hm.remove("tile_row_type"); if (s_tile_row_type == null || (!s_tile_row_type.equals("tms") && !s_tile_row_type.equals("osm"))) { // Until this is accepted, other application will get // 'confused' when the non-tms numbering is used s_tile_row_type = "tms"; } return new MbTilesMetadata(name, description, type, version, format, bounds, center, minZoom, maxZoom, s_tile_row_type, hm); } } public static class ValidatorHelper { // left, bottom, right, top | Full earth: -180.0,-85,180,85 // ----------------------------------------------- /** * Parse Bounds * - Format (Wsg84) : left/west bottom/south right/east top/north * - Full earth: -180.0,-85,180,85 * -- if not set : Full earth: -180.0,-85,180,85 will be used * @param tmp value read by the Validator * @return float[] with the left/west bottom/south right/east top/north positions */ public static float[] parseBounds( String tmp ) { float[] bounds; if (tmp == null) return null; String[] splitted = tmp.split(","); if (splitted.length != 4) return null; bounds = new float[4]; try { for( int i = 0; i < splitted.length; i++ ) { bounds[i] = Float.parseFloat(splitted[i]); } } catch (NumberFormatException e) { return null; } if ((bounds[0] >= -180.0f && bounds[0] <= 180.0f) && (bounds[2] >= -180.0f && bounds[2] <= 180.0f) && (bounds[1] >= -85.05113f && bounds[1] <= 85.05113f) && (bounds[3] >= -85.05113f && bounds[3] <= 85.05113f)) return bounds; else return null; } // ----------------------------------------------- /** * Parse default Center Position and Zoom-Level * - Format (Wsg84) : center_x, center_y, zoom * - tilemill specific parameter * - the map designer can determin the 'main point of interest' and desired zoom level * -- which may NOT be the center of the map OR the minZoom [which are the default] * - the application will only use this point when the present point is outside the bounds * @param tmp value read by the Validator * @return float[] with the x,y,z position */ public static float[] parseCenter( String tmp ) { if (tmp == null) return null; String[] splitted = tmp.split(","); if (splitted.length < 2) return null; float[] center = new float[3]; center[2] = 0; // just in case a zoom parameter is missing try { for( int i = 0; i < splitted.length; i++ ) { center[i] = Float.parseFloat(splitted[i]); } } catch (NumberFormatException e) { return null; } if ((center[0] >= -180.0f && center[0] <= 180.0f) && (center[1] >= -85.05113f && center[1] <= 85.05113f)) return center; else return null; } } // ----------------------------------------------- /** * Function to check if inserted tile is outside known bounds and min/max zoom level * @param tileBounds area to check - left/west bottom/south right/east top/north * @param i_zoom the value for zoom_level to check * @return 0=inside valid area/zoom ; i_rc > 0 outside area or zoom ; i_parm=0 no corrections ; 1= correct tileBounds values. */ public HashMap<String, String> checkTileLocation( double[] tileBounds, int i_zoom ) { // mj10777: i_rc=0=inside valid area/zoom ; i_rc > 0 outside area or // zoom ; i_parm=0 no corrections ; 1= correct tileBounds values. // int i_rc = 0; // inside area HashMap<String, String> update_metadata = new LinkedHashMap<String, String>(); double bounds_west = (double) bounds[0]; double bounds_south = (double) bounds[1]; double bounds_east = (double) bounds[2]; double bounds_north = (double) bounds[3]; double tile_west = tileBounds[0]; double tile_south = tileBounds[1]; double tile_east = tileBounds[2]; double tile_north = tileBounds[3]; int maxZoom = this.minZoom; int minZoom = this.maxZoom; if (((tile_west < bounds_west) || (tile_east > bounds_east)) || ((tile_south < bounds_south) || (tile_north > bounds_north)) || ((i_zoom < minZoom) || (i_zoom > maxZoom))) { if (((tile_west >= bounds_west) && (tile_east <= bounds_east)) && ((tile_south >= bounds_south) && (tile_north <= bounds_north))) { // We are inside the Map-Area, but Zoom is not correct if (i_zoom < minZoom) { update_metadata.put("minzoom", String.valueOf(i_zoom)); } if (i_zoom > maxZoom) { update_metadata.put("maxzoom", String.valueOf(i_zoom)); } } else { if (i_zoom < minZoom) { update_metadata.put("minzoom", String.valueOf(i_zoom)); } if (i_zoom > maxZoom) { update_metadata.put("maxzoom", String.valueOf(i_zoom)); } if (tile_west < bounds_west) { bounds_west = tile_west; } if (tile_east > bounds_east) { bounds_east = tile_east; } if (tile_south < bounds_south) { bounds_south = tile_south; } if (tile_north > bounds_north) { bounds_north = tile_north; } update_metadata.put("bounds", bounds_west + "," + bounds_south + "," + bounds_east + "," + bounds_north); } } return update_metadata; } }