@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 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;
    }
  }
  @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 onCreateOptionsMenu(final Menu pMenu) {
    pMenu.add(0, MENU_ZOOMIN_ID, Menu.NONE, "ZoomIn");
    pMenu.add(0, MENU_ZOOMOUT_ID, Menu.NONE, "ZoomOut");

    final SubMenu subMenu =
        pMenu.addSubMenu(0, MENU_TILE_SOURCE_ID, Menu.NONE, "Choose Tile Source");
    {
      for (final ITileSource tileSource : TileSourceFactory.getTileSources()) {
        subMenu.add(
            0, 1000 + tileSource.ordinal(), Menu.NONE, tileSource.localizedName(mResourceProxy));
      }
    }

    pMenu.add(0, MENU_ANIMATION_ID, Menu.NONE, "Run Animation");
    pMenu.add(0, MENU_MINIMAP_ID, Menu.NONE, "Toggle Minimap");

    return true;
  }
  @Override
  public boolean onMenuItemSelected(final int featureId, final MenuItem item) {
    switch (item.getItemId()) {
      case MENU_ZOOMIN_ID:
        this.mOsmvController.zoomIn();
        return true;

      case MENU_ZOOMOUT_ID:
        this.mOsmvController.zoomOut();
        return true;

      case MENU_TILE_SOURCE_ID:
        this.mOsmv.invalidate();
        return true;

      case MENU_MINIMAP_ID:
        mMiniMapOverlay.setEnabled(!mMiniMapOverlay.isEnabled());
        this.mOsmv.invalidate();
        return true;

      case MENU_ANIMATION_ID:
        // this.mOsmv.getController().animateTo(52370816, 9735936,
        // MapControllerOld.AnimationType.MIDDLEPEAKSPEED,
        // MapControllerOld.ANIMATION_SMOOTHNESS_HIGH,
        // MapControllerOld.ANIMATION_DURATION_DEFAULT); // Hannover
        // Stop the Animation after 500ms (just to show that it works)
        // new Handler().postDelayed(new Runnable(){
        // @Override
        // public void run() {
        // SampleExtensive.this.mOsmv.getController().stopAnimation(false);
        // }
        // }, 500);
        return true;

      default:
        ITileSource tileSource = TileSourceFactory.getTileSource(item.getItemId() - 1000);
        mOsmv.setTileSource(tileSource);
        mMiniMapOverlay.setTileSource(tileSource);
    }
    return false;
  }
/**
 * 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();

  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;
  static final 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 static final 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
  protected void draw(Canvas c, MapView osmv, boolean shadow) {

    if (DEBUGMODE) {
      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 (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);
    }
  }

  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 (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() {}
      };

  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;
  }
}