package org.osmdroid.views.overlay;
import org.osmdroid.config.Configuration;
import org.osmdroid.library.R;
import org.osmdroid.tileprovider.BitmapPool;
import org.osmdroid.tileprovider.MapTile;
import org.osmdroid.tileprovider.MapTileProviderBase;
import org.osmdroid.tileprovider.ReusableBitmapDrawable;
import org.osmdroid.tileprovider.tilesource.ITileSource;
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.osmdroid.util.TileLooper;
import org.osmdroid.util.TileSystem;
import org.osmdroid.views.MapView;
import org.osmdroid.views.Projection;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import org.osmdroid.api.IMapView;
/**
* A {@link TilesOverlay} is responsible to display a {@link MapTile}.
*
* These objects are the principle consumer of map tiles.
*
* see {@link MapTile} for an overview of how tiles are acquired by this overlay.
*
*/
public class TilesOverlay extends Overlay implements IOverlayMenuProvider {
public static final int MENU_MAP_MODE = getSafeMenuId();
public static final int MENU_TILE_SOURCE_STARTING_ID = getSafeMenuIdSequence(TileSourceFactory
.getTileSources().size());
public static final int MENU_OFFLINE = getSafeMenuId();
private Context ctx;
/** Current tile source */
protected final MapTileProviderBase mTileProvider;
protected Drawable userSelectedLoadingDrawable = null;
/* to avoid allocations during draw */
protected final Paint mDebugPaint = new Paint();
private final Rect mTileRect = new Rect();
private final Point mTilePoint = new Point();
private final Rect mViewPort = new Rect();
private Point mTopLeftMercator = new Point();
private Point mBottomRightMercator = new Point();
private Point mTilePointMercator = new Point();
private Projection mProjection;
private boolean mOptionsMenuEnabled = true;
/** A drawable loading tile **/
private BitmapDrawable mLoadingTile = null;
private int mLoadingBackgroundColor = Color.rgb(216, 208, 208);
private int mLoadingLineColor = Color.rgb(200, 192, 192);
/** For overshooting the tile cache **/
private int mOvershootTileCache = 0;
//Issue 133 night mode
private ColorFilter currentColorFilter=null;
final static float[] negate ={
-1.0f,0,0,0,255, //red
0,-1.0f,0,0,255,//green
0,0,-1.0f,0,255,//blue
0,0,0,1.0f,0 //alpha
};
/**
* provides a night mode like affect by inverting the map tile colors
*/
public final static ColorFilter INVERT_COLORS = new ColorMatrixColorFilter(negate);
public TilesOverlay(final MapTileProviderBase aTileProvider, final Context aContext) {
super();
this.ctx=aContext;
if (aTileProvider == null) {
throw new IllegalArgumentException(
"You must pass a valid tile provider to the tiles overlay.");
}
this.mTileProvider = aTileProvider;
}
/**
* See issue https://github.com/osmdroid/osmdroid/issues/330
* customizable override for the grey grid
* @since 5.2+
* @param drawable
*/
public void setLoadingDrawable(final Drawable drawable){
userSelectedLoadingDrawable = drawable;
}
@Override
public void onDetach(final MapView pMapView) {
this.mTileProvider.detach();
ctx=null;
if (mLoadingTile!=null) {
// Only recycle if we are running on a project less than 2.3.3 Gingerbread.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
if (mLoadingTile instanceof BitmapDrawable) {
final Bitmap bitmap = ((BitmapDrawable) mLoadingTile).getBitmap();
if (bitmap != null) {
bitmap.recycle();
}
}
}
if (mLoadingTile instanceof ReusableBitmapDrawable)
BitmapPool.getInstance().returnDrawableToPool((ReusableBitmapDrawable) mLoadingTile);
}
mLoadingTile=null;
if (userSelectedLoadingDrawable!=null){
// Only recycle if we are running on a project less than 2.3.3 Gingerbread.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
if (userSelectedLoadingDrawable instanceof BitmapDrawable) {
final Bitmap bitmap = ((BitmapDrawable) userSelectedLoadingDrawable).getBitmap();
if (bitmap != null) {
bitmap.recycle();
}
}
}
if (userSelectedLoadingDrawable instanceof ReusableBitmapDrawable)
BitmapPool.getInstance().returnDrawableToPool((ReusableBitmapDrawable) userSelectedLoadingDrawable);
}
userSelectedLoadingDrawable=null;
}
public int getMinimumZoomLevel() {
return mTileProvider.getMinimumZoomLevel();
}
public int getMaximumZoomLevel() {
return mTileProvider.getMaximumZoomLevel();
}
/**
* Whether to use the network connection if it's available.
*/
public boolean useDataConnection() {
return mTileProvider.useDataConnection();
}
/**
* Set whether to use the network connection if it's available.
*
* @param aMode
* if true use the network connection if it's available. if false don't use the
* network connection even if it's available.
*/
public void setUseDataConnection(final boolean aMode) {
mTileProvider.setUseDataConnection(aMode);
}
@Override
public void draw(Canvas c, MapView osmv, boolean shadow) {
if (Configuration.getInstance().isDebugTileProviders()) {
Log.d(IMapView.LOGTAG,"onDraw(" + shadow + ")");
}
if (shadow) {
return;
}
Projection projection = osmv.getProjection();
// Get the area we are drawing to
Rect screenRect = projection.getScreenRect();
//No overflow detected here! Log.d(IMapView.LOGTAG, "BEFORE Rect is " + screenRect.toString() + mTopLeftMercator.toString());
projection.toMercatorPixels(screenRect.left, screenRect.top, mTopLeftMercator);
projection.toMercatorPixels(screenRect.right, screenRect.bottom, mBottomRightMercator);
//No overflow detected here! Log.d(IMapView.LOGTAG, "AFTER Rect is " + screenRect.toString() + mTopLeftMercator.toString() + mBottomRightMercator.toString());
mViewPort.set(mTopLeftMercator.x, mTopLeftMercator.y, mBottomRightMercator.x,
mBottomRightMercator.y);
//No overflow detected here! Log.d(IMapView.LOGTAG, "AFTER Rect is " + mViewPort.toString());
// Draw the tiles!
drawTiles(c, projection, projection.getZoomLevel(), TileSystem.getTileSize(), mViewPort);
}
/**
* This is meant to be a "pure" tile drawing function that doesn't take into account
* osmdroid-specific characteristics (like osmdroid's canvas's having 0,0 as the center rather
* than the upper-left corner). Once the tile is ready to be drawn, it is passed to
* onTileReadyToDraw where custom manipulations can be made before drawing the tile.
*/
public void drawTiles(final Canvas c, final Projection projection, final int zoomLevel,
final int tileSizePx, final Rect viewPort) {
mProjection = projection;
mTileLooper.loop(c, zoomLevel, tileSizePx, viewPort);
// draw a cross at center in debug mode
if (Configuration.getInstance().isDebugTileProviders()) {
// final GeoPoint center = osmv.getMapCenter();
final Point centerPoint = new Point(viewPort.centerX(), viewPort.centerY());
c.drawLine(centerPoint.x, centerPoint.y - 9, centerPoint.x, centerPoint.y + 9, mDebugPaint);
c.drawLine(centerPoint.x - 9, centerPoint.y, centerPoint.x + 9, centerPoint.y, mDebugPaint);
}
}
private final TileLooper mTileLooper = new TileLooper() {
@Override
public void initialiseLoop(final int pZoomLevel, final int pTileSizePx) {
// make sure the cache is big enough for all the tiles
final int numNeeded = (mLowerRight.y - mUpperLeft.y + 1) * (mLowerRight.x - mUpperLeft.x + 1);
mTileProvider.ensureCapacity(numNeeded + mOvershootTileCache);
}
@Override
public void handleTile(final Canvas pCanvas, final int pTileSizePx, final MapTile pTile, final int pX, final int pY) {
//no overflow detected here Log.d(IMapView.LOGTAG, "handleTile " + pTile.toString() + ","+pX + "," + pY);
Drawable currentMapTile = mTileProvider.getMapTile(pTile);
boolean isReusable = currentMapTile instanceof ReusableBitmapDrawable;
final ReusableBitmapDrawable reusableBitmapDrawable =
isReusable ? (ReusableBitmapDrawable) currentMapTile : null;
if (currentMapTile == null) {
currentMapTile = getLoadingTile();
}
if (currentMapTile != null) {
mTilePoint.set(pX * pTileSizePx, pY * pTileSizePx);
mTileRect.set(mTilePoint.x, mTilePoint.y, mTilePoint.x + pTileSizePx, mTilePoint.y
+ pTileSizePx);
if (isReusable) {
reusableBitmapDrawable.beginUsingDrawable();
}
try {
if (isReusable && !((ReusableBitmapDrawable) currentMapTile).isBitmapValid()) {
currentMapTile = getLoadingTile();
isReusable = false;
}
onTileReadyToDraw(pCanvas, currentMapTile, mTileRect);
} finally {
if (isReusable)
reusableBitmapDrawable.finishUsingDrawable();
}
}
if (Configuration.getInstance().isDebugTileProviders()) {
mTileRect.set(pX * pTileSizePx, pY * pTileSizePx, pX * pTileSizePx + pTileSizePx, pY
* pTileSizePx + pTileSizePx);
pCanvas.drawText(pTile.toString(), mTileRect.left + 1,
mTileRect.top + mDebugPaint.getTextSize(), mDebugPaint);
pCanvas.drawLine(mTileRect.left, mTileRect.top, mTileRect.right, mTileRect.top,
mDebugPaint);
pCanvas.drawLine(mTileRect.left, mTileRect.top, mTileRect.left, mTileRect.bottom,
mDebugPaint);
}
}
@Override
public void finaliseLoop() {
}
};
protected void onTileReadyToDraw(final Canvas c, final Drawable currentMapTile,
final Rect tileRect) {
currentMapTile.setColorFilter(currentColorFilter);
mProjection.toPixelsFromMercator(tileRect.left, tileRect.top, mTilePointMercator);
tileRect.offsetTo(mTilePointMercator.x, mTilePointMercator.y);
currentMapTile.setBounds(tileRect);
currentMapTile.draw(c);
}
@Override
public void setOptionsMenuEnabled(final boolean pOptionsMenuEnabled) {
this.mOptionsMenuEnabled = pOptionsMenuEnabled;
}
@Override
public boolean isOptionsMenuEnabled() {
return this.mOptionsMenuEnabled;
}
@Override
public boolean onCreateOptionsMenu(final Menu pMenu, final int pMenuIdOffset,
final MapView pMapView) {
final SubMenu mapMenu = pMenu.addSubMenu(0, MENU_MAP_MODE + pMenuIdOffset, Menu.NONE,
R.string.map_mode).setIcon(R.drawable.ic_menu_mapmode);
for (int a = 0; a < TileSourceFactory.getTileSources().size(); a++) {
final ITileSource tileSource = TileSourceFactory.getTileSources().get(a);
mapMenu.add(MENU_MAP_MODE + pMenuIdOffset, MENU_TILE_SOURCE_STARTING_ID + a
+ pMenuIdOffset, Menu.NONE, tileSource.name());
}
mapMenu.setGroupCheckable(MENU_MAP_MODE + pMenuIdOffset, true, true);
if (ctx!=null) {
final String title = ctx.getString(
pMapView.useDataConnection() ? R.string.set_mode_offline
: R.string.set_mode_online);
final Drawable icon = ctx.getResources().getDrawable(R.drawable.ic_menu_offline);
pMenu.add(0, MENU_OFFLINE + pMenuIdOffset, Menu.NONE, title).setIcon(icon);
}
return true;
}
@Override
public boolean onPrepareOptionsMenu(final Menu pMenu, final int pMenuIdOffset,
final MapView pMapView) {
final int index = TileSourceFactory.getTileSources().indexOf(
pMapView.getTileProvider().getTileSource());
if (index >= 0) {
pMenu.findItem(MENU_TILE_SOURCE_STARTING_ID + index + pMenuIdOffset).setChecked(true);
}
pMenu.findItem(MENU_OFFLINE + pMenuIdOffset).setTitle(
pMapView.useDataConnection() ? R.string.set_mode_offline
: R.string.set_mode_online);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem pItem, final int pMenuIdOffset,
final MapView pMapView) {
final int menuId = pItem.getItemId() - pMenuIdOffset;
if ((menuId >= MENU_TILE_SOURCE_STARTING_ID)
&& (menuId < MENU_TILE_SOURCE_STARTING_ID
+ TileSourceFactory.getTileSources().size())) {
pMapView.setTileSource(TileSourceFactory.getTileSources().get(
menuId - MENU_TILE_SOURCE_STARTING_ID));
return true;
} else if (menuId == MENU_OFFLINE) {
final boolean useDataConnection = !pMapView.useDataConnection();
pMapView.setUseDataConnection(useDataConnection);
return true;
} else {
return false;
}
}
public int getLoadingBackgroundColor() {
return mLoadingBackgroundColor;
}
/**
* Set the color to use to draw the background while we're waiting for the tile to load.
*
* @param pLoadingBackgroundColor
* the color to use. If the value is {@link Color#TRANSPARENT} then there will be no
* loading tile.
*/
public void setLoadingBackgroundColor(final int pLoadingBackgroundColor) {
if (mLoadingBackgroundColor != pLoadingBackgroundColor) {
mLoadingBackgroundColor = pLoadingBackgroundColor;
clearLoadingTile();
}
}
public int getLoadingLineColor() {
return mLoadingLineColor;
}
public void setLoadingLineColor(final int pLoadingLineColor) {
if (mLoadingLineColor != pLoadingLineColor) {
mLoadingLineColor = pLoadingLineColor;
clearLoadingTile();
}
}
private Drawable getLoadingTile() {
if (userSelectedLoadingDrawable!=null)
return userSelectedLoadingDrawable;
if (mLoadingTile == null && mLoadingBackgroundColor != Color.TRANSPARENT) {
try {
final int tileSize = mTileProvider.getTileSource() != null ? mTileProvider
.getTileSource().getTileSizePixels() : 256;
final Bitmap bitmap = Bitmap.createBitmap(tileSize, tileSize,
Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
final Paint paint = new Paint();
canvas.drawColor(mLoadingBackgroundColor);
paint.setColor(mLoadingLineColor);
paint.setStrokeWidth(0);
final int lineSize = tileSize / 16;
for (int a = 0; a < tileSize; a += lineSize) {
canvas.drawLine(0, a, tileSize, a, paint);
canvas.drawLine(a, 0, a, tileSize, paint);
}
mLoadingTile = new BitmapDrawable(bitmap);
} catch (final OutOfMemoryError e) {
Log.e(IMapView.LOGTAG, "OutOfMemoryError getting loading tile");
System.gc();
} catch (final NullPointerException e) {
Log.e(IMapView.LOGTAG, "NullPointerException getting loading tile");
System.gc();
}
}
return mLoadingTile;
}
private void clearLoadingTile() {
final BitmapDrawable bitmapDrawable = mLoadingTile;
mLoadingTile = null;
// Only recycle if we are running on a project less than 2.3.3 Gingerbread.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
if (bitmapDrawable != null) {
bitmapDrawable.getBitmap().recycle();
}
}
}
/**
* Set this to overshoot the tile cache. By default the TilesOverlay only creates a cache large
* enough to hold the minimum number of tiles necessary to draw to the screen. Setting this
* value will allow you to overshoot the tile cache and allow more tiles to be cached. This
* increases the memory usage, but increases drawing performance.
*
* @param overshootTileCache
* the number of tiles to overshoot the tile cache by
*/
public void setOvershootTileCache(int overshootTileCache) {
mOvershootTileCache = overshootTileCache;
}
/**
* Get the tile cache overshoot value.
*
* @return the number of tiles to overshoot tile cache
*/
public int getOvershootTileCache() {
return mOvershootTileCache;
}
/**
* sets the current color filter, which is applied to tiles before being drawn to the screen.
* Use this to enable night mode or any other tile rendering adjustment as necessary. use null to clear.
* INVERT_COLORS provides color inversion for convenience and to support the previous night mode
* @param filter
* @since 5.1
*/
public void setColorFilter(ColorFilter filter) {
this.currentColorFilter=filter;
}
}