/* * GeoSolutions map - Digital field mapping on Android based devices * Copyright (C) 2014 GeoSolutions (www.geo-solutions.it) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package it.geosolutions.android.map.mbtiles; import it.geosolutions.android.map.BuildConfig; import it.geosolutions.android.map.renderer.OverlayRenderer; import it.geosolutions.android.map.style.AdvancedStyle; import it.geosolutions.android.map.utils.StyleUtils; import java.util.ArrayList; import org.mapsforge.android.maps.Projection; import org.mapsforge.core.model.BoundingBox; import org.mapsforge.core.model.GeoPoint; import org.mapsforge.core.model.Tile; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.os.Build; import android.util.Log; import android.util.TimingLogger; import eu.geopaparazzi.spatialite.database.spatial.core.ISpatialDatabaseHandler; /** * A Renderer for MBTiles Layers * * @author Lorenzo Pini (lorenzo.pini@geo-solutions.it) * */ public class MbTilesRenderer implements OverlayRenderer<MbTilesLayer> { // ----------------------------------------------- // copied from // - geopaparazzilibrary/src/eu/geopaparazzi/library/util/Utilities.java // ----------------------------------------------- public static double originShift = 2 * Math.PI * 6378137 / 2.0; /** * first zoom level from which zooms are launched */ private static int MIN_ZOOM_LEVEL_TO_ZOOM = 11; /** * min zoom level difference that is zoomed * eg. a zoom level x tile is cut out one quarter * and scaled up to fit zoom level x + ZOOM_LEVEL_DIFFERENCE_TO_ZOOM */ private static int MIN_ZOOM_LEVEL_DIFFERENCE = 1; /** * max zoom level difference for which zoomed tiles are produced */ private static int MAX_ZOOM_LEVEL_DIFFERENCE = 3; private ArrayList<MbTilesLayer> layers; private Projection projection; /** * Tag for Logging */ private static String TAG = "MbTilesRenderer"; /** * Draws the tiles of the requested bounding box at the requested zoom level * to the given {@link Canvas} */ public void render(Canvas c, BoundingBox boundingBox, byte zoomLevel) { // Time execution TimingLogger timings = new TimingLogger(TAG, "Rendering MBTiles"); // actual rendering drawFromMbTile(c, boundingBox, zoomLevel); // measure and log execution time timings.addSplit("Render Done"); timings.dumpToLog(); } @Override public void setLayers(ArrayList<MbTilesLayer> layers) { this.layers = layers; } public void refresh() { // nothing to update } @Override public ArrayList<MbTilesLayer> getLayers() { return layers; } /** * Draws the tiles of the given {@link BoundingBox} in the given {@link Canvas} * @param canvas * @param boundingBox * @param drawZoomLevel */ private void drawFromMbTile (Canvas canvas, BoundingBox boundingBox,final byte drawZoomLevel) { double n = boundingBox.maxLatitude; double w = boundingBox.minLongitude; double s = boundingBox.minLatitude; double e = boundingBox.maxLongitude; if(projection == null){ // cannot continue return; } // Get the point of the canvas where to start drawing GeoPoint dp = new GeoPoint(n, w); // UpperLeftCorner //long[] pxDp = ProjectionUtils.getDrawPoint(dp, projection, drawZoomLevel); //long drawX = pxDp[0]; //long drawY = pxDp[1]; android.graphics.Point bboxPixelPoint = projection.toPixels(dp, null); // Get the row/col values of the tiles covering the area to draw int[] tile_bounds = LatLonBounds_to_TileBounds( new double[]{w,n,e,s}, drawZoomLevel); 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]; // get tileLatLonBounds of the upper left tile double[] tb = tileLatLonBounds(i_min_x, i_min_y_osm, drawZoomLevel, Tile.TILE_SIZE); // Check Lon/Lat bounds // if( -180 > tb[0] || tb[0] > 180 // || -90 > tb[1] || tb[1] > 90){ // // Computations gone wrong, skip // return; // } GeoPoint mbtileUlc = new GeoPoint(tb[1], tb[0]); // UpperLeftCorner //long[] pxMbtile = ProjectionUtils.getDrawPoint(mbtileUlc, projection, drawZoomLevel); //long tileX = pxMbtile[0]; //long tileY = pxMbtile[1]; // get the screen pixel position android.graphics.Point tilePixelPoint = projection.toPixels(mbtileUlc, null); /* if(BuildConfig.DEBUG){ Log.v(TAG, "BBox Point [ "+bboxPixelPoint.x+" , "+bboxPixelPoint.y+" ]"); Log.v(TAG, "Tile Point [ "+tilePixelPoint.x+" , "+tilePixelPoint.y+" ]"); } */ long offsetX = (long) bboxPixelPoint.x - tilePixelPoint.x; long offsetY = (long) bboxPixelPoint.y - tilePixelPoint.y; StringBuilder sb = new StringBuilder(); for (MbTilesLayer l : layers) { // visibility checks if (!l.isVisibility()) continue; if (isInterrupted() || sizeHasChanged()) { // stop working return; } // check visibility range in style AdvancedStyle style4Table = l.getStyle(); if (!StyleUtils.isInVisibilityRange(style4Table, drawZoomLevel)) { continue; } // retrieve the handler //SpatialRasterTable spatialRasterTable = sdManager.getRasterTableByName(l.getTableName()); ISpatialDatabaseHandler spatialDatabaseHandler = l.getSpatialDatabaseHandler(); if (spatialDatabaseHandler != null) { //////// // Prepare the pixel matrix int[] pixels = new int[Tile.TILE_SIZE * Tile.TILE_SIZE]; Bitmap decodedBitmap = null; Bitmap bitmap = null; Paint paint = new Paint(); try { paint.setAlpha((int) l.getOpacity()); // set transparent value here }catch(ClassCastException cce){ if(BuildConfig.DEBUG){ Log.w(TAG, "Cannot cast opacity to INT"); } } for(int tile_y = i_min_y_osm; tile_y<=i_max_y_osm; tile_y++ ){ for(int tile_x = i_min_x; tile_x<=i_max_x; tile_x++ ){ // clear all sb.delete(0, sb.length()); byte[] rasterBytes = null; sb.append(drawZoomLevel).append(",").append(tile_x).append(",").append(tile_y); String tileQuery = sb.toString(); // get the raster tile rasterBytes = spatialDatabaseHandler.getRasterTile(tileQuery); if( rasterBytes == null){ // got nothing, check if we can interpolate a desired tile out of an available tile //if no difference or low zoom level, continue if(MIN_ZOOM_LEVEL_DIFFERENCE == 0 || drawZoomLevel < MIN_ZOOM_LEVEL_TO_ZOOM){ continue; } int currentZoomLevel = drawZoomLevel; double[] latlon = tileXYToLatLon(tile_x, tile_y, drawZoomLevel); int zoomLevelDiff = 0; int newCoords[] = null; int count = 0; //decrease the zoom level and try to fetch a tile while(rasterBytes == null){ if(count > MAX_ZOOM_LEVEL_DIFFERENCE)break; //give up currentZoomLevel--; sb.delete(0, sb.length()); //convert original coordinates to coordinates according to this current zoom level newCoords = latLonZoomToTileXY(latlon[0], latlon[1], currentZoomLevel); sb.append(currentZoomLevel).append(",").append(newCoords[0]).append(",").append(newCoords[1]); String newestTileQuery = sb.toString(); rasterBytes = spatialDatabaseHandler.getRasterTile(newestTileQuery); if(rasterBytes != null){//found, remember properties zoomLevelDiff = drawZoomLevel - currentZoomLevel; } count++; } if(rasterBytes == null){//we gave up finding something continue; } //we have raster data, check zoom level difference if(zoomLevelDiff > MIN_ZOOM_LEVEL_DIFFERENCE){ //for large differences between available data and wanted zoom level //an interpolation would not make anymore sense, continue continue; } //decoded the byte to get the pixels decodedBitmap = BitmapFactory.decodeByteArray(rasterBytes, 0, rasterBytes.length); decodedBitmap.getPixels(pixels, 0, Tile.TILE_SIZE, 0, 0, Tile.TILE_SIZE, Tile.TILE_SIZE); int[] foundPixels = findPixels(pixels, tile_x, tile_y, drawZoomLevel, newCoords[0], newCoords[1], currentZoomLevel); // interpolate until 256*256, prepared for more levels of zoomdifference int[] tempPixels = foundPixels; while(true){ int[] scaledPixels = scalePixels(foundPixels); tempPixels = scaledPixels; //Log.d(TAG, "scaling, now " + tempPixels.length); if(tempPixels.length == (Tile.TILE_SIZE * Tile.TILE_SIZE)){ break; } } // if(BuildConfig.DEBUG){ // Log.d(TAG, String.format("tile zoomed %d %d %d",newCoords[0], newCoords[1], currentZoomLevel)); // } pixels = tempPixels; }else{ //rasterBytes found // if(BuildConfig.DEBUG){ // Log.d(TAG, String.format("native tile %d %d %d",tile_x,tile_y,drawZoomLevel)); // } decodedBitmap = BitmapFactory.decodeByteArray(rasterBytes, 0, rasterBytes.length); // check if the input stream could be decoded into a bitmap if (decodedBitmap != null) { // copy all pixels from the decoded bitmap to the color array decodedBitmap.getPixels(pixels, 0, Tile.TILE_SIZE, 0, 0, Tile.TILE_SIZE, Tile.TILE_SIZE); decodedBitmap.recycle(); } else { for (int i = 0; i < pixels.length; i++) { pixels[i] = Color.WHITE; } } } if(bitmap == null){ Bitmap.Config conf = Bitmap.Config.ARGB_8888; bitmap = Bitmap.createBitmap(Tile.TILE_SIZE, Tile.TILE_SIZE, conf); } // copy all pixels from the color array to the tile bitmap bitmap.setPixels(pixels, 0, Tile.TILE_SIZE, 0, 0, Tile.TILE_SIZE, Tile.TILE_SIZE); // do the actual drawing on canvas /* * TODO: investigate the why the tiles must be drawn with an additional Y offset of (-tileSize) * ((tile_y - i_min_y_osm -1 )*tileSize) -offsetY * instead of * ((tile_y - i_min_y_osm )*tileSize) -offsetY * */ canvas.drawBitmap(bitmap, ((tile_x-i_min_x)*Tile.TILE_SIZE)-offsetX, ((tile_y - i_min_y_osm - 1)*Tile.TILE_SIZE)-offsetY, paint); /* if(BuildConfig.DEBUG){ Log.v(TAG, sb.toString()); } */ } } if(bitmap != null){ bitmap.recycle(); } //////// } } /* } catch (Exception e1) { if (BuildConfig.DEBUG) { Log.e(TAG, "Exception while rendering spatialite data"); Log.e(TAG, e1.getLocalizedMessage(), e1); } } */ } private boolean sizeHasChanged() { return false; } private boolean isInterrupted() { return false; } public void setProjection(Projection p) { this.projection = p; } /** * <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 latlong_bounds [position_y,position_x] * @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] }; } /** * Returns bounds of the given tile in EPSG:900913 coordinates * * <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) { //cast to long needed to go over the 24th zoom level (integer limit overflow) double[] min = pixelsToMeters((long)tx * (long)tileSize, (long)ty * (long)tileSize, zoom, tileSize); double minx = min[0], miny = min[1]; double[] max = pixelsToMeters(((long)tx + (long)1) * (long)tileSize, ((long)ty + (long)1) * (long)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> * 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 }; } /** * Resolution (meters/pixel) for given zoom level (measured at Equator) * * <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); } /** * <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] }; } /** * Converts XY point from Spherical Mercator EPSG:900913 to lat/lon in WGS84 Datum * * <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 }; } /** * from http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames * converts a tiles x,y and zoom to the position of the tiles upper left corner * @param tile_x * @param tile_y * @param zoom * @return */ public static double[] tileXYToLatLon(int tile_x_, int tile_y_, byte zoom){ //\quote {http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames // // Normally this returns the NW-corner of the square. // Use the function with xtile+1 and/or ytile+1 to get the other corners. // With xtile+0.5 & ytile+0.5 it will return the center of the tile. //} // calculates the center position of the tile double tile_x = tile_x_ + 0.5d; double tile_y = tile_y_ + 0.5d; double lon = tile_x / Math.pow(2.0, zoom) * 360.0 - 180; double n = Math.PI - (2.0 * Math.PI * tile_y) / Math.pow(2.0, zoom); double lat = Math.toDegrees(Math.atan(Math.sinh(n))); return new double[]{lat,lon}; } /** * from http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames * converts lat/lon and zoom to a tiles x and y according to this zoom level * @param lat * @param lon * @param zoom * @return */ public static int[] latLonZoomToTileXY(final double lat, final double lon, final int zoom) { int xtile = (int)Math.floor( (lon + 180) / 360 * (1<<zoom) ) ; int ytile = (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 < 0) ytile=0; if (ytile >= (1<<zoom)) ytile=((1<<zoom)-1); return new int[] { xtile , ytile}; } public enum OnTilePosition { UPPER_LEFT, UPPER_RIGHT, LOWER_LEFT, LOWER_RIGHT; } /** * converts the deltas of the comparison of two tiles source positions * and returns the allegedly position of the lower zoomed tile on the higher zoomed tile * @param latDelta * @param lonDelta * @return */ public static OnTilePosition latlonDeltasToPositionOnTile(double latDelta, double lonDelta){ //now seen from center if(latDelta > 0 && lonDelta < 0){ return OnTilePosition.UPPER_LEFT; }else if(latDelta > 0 && lonDelta > 0){ return OnTilePosition.UPPER_RIGHT; }else if (latDelta < 0 && lonDelta < 0 ){ return OnTilePosition.LOWER_LEFT; }else if (latDelta < 0 && lonDelta > 0){ return OnTilePosition.LOWER_RIGHT; }else{ throw new IllegalArgumentException("illegal deltas calculated lat : "+latDelta+" lon : "+lonDelta); } } /** * cuts out the pixels of a region of the origpixels and returns them cut off area * @param origPixels the original pixels * @param onTilePos the position to cut the pixels out * @return the pixels of the cut out region */ public static int[] cutOutPixels(final int[] origPixels, final OnTilePosition onTilePos){ int[] newPixels = new int[origPixels.length / 4]; int width = (int) Math.sqrt(origPixels.length); int x_start = 0, y_start = 0, x_end = 0, y_end = 0; switch (onTilePos) { case UPPER_LEFT: x_end = width / 2; y_end = width / 2; break; case UPPER_RIGHT : x_start = width / 2; x_end = width; y_end = width / 2; break; case LOWER_LEFT: x_end = width / 2; y_start = width / 2; y_end = width; break; case LOWER_RIGHT : x_start = width / 2; x_end = width; y_start = width / 2; y_end = width; break; default: throw new IllegalArgumentException("invalid onttile position"); } for (int y_new = 0, y_old = y_start; y_old < y_end; y_new++, y_old++) { for (int x_new = 0, x_old = x_start; x_old < x_end; x_new++,x_old++) { int oldpos = y_old * width + x_old; int newpos = y_new * (width / 2) + x_new; newPixels[newpos] = origPixels[oldpos]; } } return newPixels; } /** * method to extract pixels for a desired position and zoomlevel out of a found tiles data * it goes down the zoom level difference and extracts pixels of the desired area until * the desired position is covered * * @param origPixels * @param origX * @param origY * @param origZoom * @param foundX * @param foundY * @param foundZoom * @return the extracted pixels */ public static int[] findPixels(final int[] origPixels, final int origX, final int origY, final int origZoom, final int foundX, final int foundY, final int foundZoom){ final double[] origLatlon = tileXYToLatLon(origX, origY,(byte) origZoom); int[] currentTileCoords = new int[]{foundX, foundY}; int currentZoomLevel = foundZoom; int[] currentPixels = origPixels.clone(); while(currentZoomLevel != origZoom){ //calculate pos of current tile double[] currentLatlon = tileXYToLatLon(currentTileCoords[0], currentTileCoords[1],(byte) currentZoomLevel); //check deltas to OnTilePos double latDiff = origLatlon[0] - currentLatlon[0]; double lonDiff = origLatlon[1] - currentLatlon[1]; //cut out pixels of pos final OnTilePosition pos = latlonDeltasToPositionOnTile(latDiff, lonDiff); //cut out the current quarter int[] tempPixels = cutOutPixels(currentPixels, pos); currentPixels = tempPixels; currentZoomLevel++; } return currentPixels; } /** * scales up an pixel array by the amount of 2 * hence an 16 pixels array will contain 64 * it is not using any interpolation or manipulation currently * but simply repeats the pixel four times * * @param sourcePixels * @return the scaled pixel array */ public static int[] scalePixels(int[] sourcePixels){ int[]scaled = new int[sourcePixels.length * 4]; int width = (int) Math.sqrt(sourcePixels.length); int newwidth = width * 2; for (int y = 0; y < width; y++) { for (int x = 0; x < width; x++) { int oldpos = y * width + x; int newpos0 = (y * 2) * newwidth + (x * 2); int newpos1 = (y * 2) * newwidth + (x * 2) + 1; int newpos2 = ((y * 2) + 1) * newwidth + (x * 2); int newpos3 = ((y * 2) + 1) * newwidth + (x * 2) + 1; scaled[newpos0] = sourcePixels[oldpos]; scaled[newpos1] = sourcePixels[oldpos]; scaled[newpos2] = sourcePixels[oldpos]; scaled[newpos3] = sourcePixels[oldpos]; } } return scaled; } }