package org.mozilla.osmdroid.views.overlay;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
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.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import org.mozilla.mozstumbler.service.core.logging.ClientLog;
import org.mozilla.mozstumbler.svclocator.services.log.LoggerUtil;
import org.mozilla.osmdroid.ResourceProxy;
import org.mozilla.osmdroid.tileprovider.MapTile;
import org.mozilla.osmdroid.tileprovider.MapTileProviderBase;
import org.mozilla.osmdroid.tileprovider.ReusableBitmapDrawable;
import org.mozilla.osmdroid.tileprovider.tilesource.ITileSource;
import org.mozilla.osmdroid.tileprovider.tilesource.TileSourceFactory;
import org.mozilla.osmdroid.util.TileLooper;
import org.mozilla.osmdroid.util.TileSystem;
import org.mozilla.osmdroid.views.MapView;
import org.mozilla.osmdroid.views.Projection;
/**
* These objects are the principle consumer of map tiles.
* <p/>
* 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();
/**
* For overshooting the tile cache *
*/
// @TODO vng: I bumped this number up while debugging the scaling
// issue. Caching in osmdroid is treated as reliable storage with
// respect to loading of tiles. The problem is that the caches
// are specified as LRUCaches backed by a LinkedHashMap, so there
// is no formal policy for when a tile is evicted from the LRU
// cache. The caches really need to just go away and we should
// load directly from storage.
public static final int OVERSHOOT_TILE_CACHE_SIZE = 0;
private static final String LOG_TAG = LoggerUtil.makeLogTag(TilesOverlay.class);
/**
* Current tile source
*/
protected final MapTileProviderBase mTileProvider;
/* to avoid allocations during draw */
protected final Paint mDebugPaint = new Paint();
private final Rect mTileRect = new Rect();
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 + OVERSHOOT_TILE_CACHE_SIZE);
}
@Override
public void handleTile(final Canvas pCanvas, final int pTileSizePx, final MapTile pTile, final int pX, final int 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 (DEBUGMODE) {
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() {
}
};
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);
public TilesOverlay(final MapTileProviderBase aTileProvider, final ResourceProxy pResourceProxy) {
super(pResourceProxy);
if (aTileProvider == null) {
throw new IllegalArgumentException(
"You must pass a valid tile provider to the tiles overlay.");
}
this.mTileProvider = aTileProvider;
}
@Override
public void onDetach(final MapView pMapView) {
this.mTileProvider.detach();
}
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
protected void draw(Canvas c, MapView osmv, boolean shadow) {
if (DEBUGMODE) {
ClientLog.d(LOG_TAG, "onDraw(" + shadow + ")");
}
if (shadow) {
return;
}
Projection projection = osmv.getProjection();
// Get the area we are drawing to
Rect screenRect = projection.getScreenRect();
projection.toMercatorPixels(screenRect.left, screenRect.top, mTopLeftMercator);
projection.toMercatorPixels(screenRect.right, screenRect.bottom, mBottomRightMercator);
mViewPort.set(mTopLeftMercator.x, mTopLeftMercator.y, mBottomRightMercator.x,
mBottomRightMercator.y);
// 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 (DEBUGMODE) {
// 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);
}
}
protected void onTileReadyToDraw(final Canvas c, final Drawable currentMapTile,
final Rect tileRect) {
mProjection.toPixelsFromMercator(tileRect.left, tileRect.top, mTilePointMercator);
tileRect.offsetTo(mTilePointMercator.x, mTilePointMercator.y);
currentMapTile.setBounds(tileRect);
currentMapTile.draw(c);
}
@Override
public boolean isOptionsMenuEnabled() {
return this.mOptionsMenuEnabled;
}
@Override
public void setOptionsMenuEnabled(final boolean pOptionsMenuEnabled) {
this.mOptionsMenuEnabled = pOptionsMenuEnabled;
}
@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,
mResourceProxy.getString(ResourceProxy.string.map_mode)).setIcon(
mResourceProxy.getDrawable(ResourceProxy.bitmap.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.localizedName(mResourceProxy));
}
mapMenu.setGroupCheckable(MENU_MAP_MODE + pMenuIdOffset, true, true);
final String title = pMapView.getResourceProxy().getString(
pMapView.useDataConnection() ? ResourceProxy.string.offline_mode
: ResourceProxy.string.online_mode);
final Drawable icon = pMapView.getResourceProxy().getDrawable(
ResourceProxy.bitmap.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.getResourceProxy().getString(
pMapView.useDataConnection() ? ResourceProxy.string.offline_mode
: ResourceProxy.string.online_mode));
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();
}
}
// @TODO vng - this can be refactored and pushed down into the
// TileProvider. All the details about the tile size are already
// in the tileprovider anyway.
private Drawable getLoadingTile() {
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) {
// OOM is 'normal' for bitmap operations on Android
ClientLog.e(LOG_TAG, "OutOfMemoryError getting loading tile", e);
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();
}
}
}
}