content: /*
   * Androzic - android navigation client that uses OziExplorer maps (ozf2, ozfx3).
   * Copyright (C) 2010-2012 Andrey Novikov <http://andreynovikov.info/>
   * 
   * This file is part of Androzic application.
   * 
   * Androzic 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.
   * 
   * Androzic 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 Androzic. If not, see <http://www.gnu.org/licenses/>.
   */
  
  package com.androzic;
  
  import java.lang.ref.WeakReference;
  
  import org.metalev.multitouch.controller.MultiTouchController;
  import org.metalev.multitouch.controller.MultiTouchController.MultiTouchObjectCanvas;
  import org.metalev.multitouch.controller.MultiTouchController.PointInfo;
  import org.metalev.multitouch.controller.MultiTouchController.PositionAndScale;
  
  import android.content.Context;
  import android.content.res.Resources;
  import android.graphics.Bitmap;
  import android.graphics.Canvas;
  import android.graphics.Color;
  import android.graphics.Matrix;
  import android.graphics.Paint;
  import android.graphics.Path;
  import android.graphics.PorterDuff;
  import android.graphics.PorterDuffColorFilter;
  import android.graphics.Rect;
  import android.graphics.Typeface;
  import android.graphics.Paint.Align;
  import android.graphics.drawable.Drawable;
  import android.location.Location;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.Message;
  import android.os.Parcelable;
  import android.os.SystemClock;
  import android.support.annotation.NonNull;
  import android.util.AttributeSet;
  import android.view.KeyEvent;
  import android.view.MotionEvent;
  import android.view.SurfaceHolder;
  import android.view.SurfaceView;
  import android.view.ViewConfiguration;
  import android.widget.Toast;
  
  import com.androzic.data.Bounds;
  import com.androzic.map.BaseMap;
  import com.androzic.overlay.MapOverlay;
  import com.androzic.overlay.OverlayManager;
  import com.androzic.ui.Viewport;
  import com.androzic.util.Geo;
  import com.androzic.util.StringFormatter;
  
  public class MapView extends SurfaceView implements SurfaceHolder.Callback, MultiTouchObjectCanvas<Object>
  {
  	private static final String TAG = "MapView";
  	
  	private static final int REFRESH_MESSAGE = 1;
  
  	private static final float MAX_ROTATION_SPEED = 20f;
  	private static final float INC_ROTATION_SPEED = 2f;
  	private static final float MAX_SHIFT_SPEED = 20f;
  	private static final float INC_SHIFT_SPEED = 2f;
  	
  	private static final int VIEWPORT_EXCESS = 64;
  
  	private static final int GESTURE_THRESHOLD_DP = (int) (ViewConfiguration.get(Androzic.getApplication()).getScaledTouchSlop() * 3);
  	private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
  
  	private static final int SCALE_MOVE_DELAY = 2000; // 2 seconds
  
  	private int vectorType = 1;
  	private int vectorMultiplier = 10;
  	private boolean strictUnfollow = true;
  	private boolean loadBestMap = true;
  	private int bestMapInterval = 5000; // 5 seconds
  	private long drawPeriod = 200; // 200 milliseconds
  	private int crossCursorHideDelay = 5000; // 5 seconds
  
  	/**
  	 * True when there is a valid location
  	 */
  	private boolean isFixed = false;
  	/**
  	 * True when there is a valid bearing
  	 */
  	private boolean isMoving = false;
  	/**
  	 * True when map moves with location cursor
  	 */
  	private boolean isFollowing = false;
  
  	private long lastBestMap = 0;
  	private boolean bestMapEnabled = true;
  
  	private GestureHandler tapHandler;
  	private long firstTapTime = 0;
  	private boolean wasDoubleTap = false;
  	private MotionEvent upEvent = null;
  	protected long lastDragTime = 0;
  	private int penX = 0;
  	private int penY = 0;
  	private int penOX = 0;
  	private int penOY = 0;
  	private int lookAhead = 0;
  	private float lookAheadC = 0;
  	private float lookAheadS = 0;
  	private float lookAheadSS = 0;
  	private int lookAheadPst = 0;
  
  	private float lookAheadB = 0;
  	private float smoothB = 0;
  	private float smoothBS = 0;
  	private double mpp = 0;
  	private int vectorLength = 0;
  	private int proximity = 0;
  	private boolean mapRotate = false;
  
  	// cursors
  	private Drawable movingCursor = null;
  	private Paint crossPaint = null;
  	private Paint pointerPaint = null;
  	private Paint compassPaint = null;
  	private int activeColor = Color.RED;
  	private PorterDuffColorFilter active = null;
  	private Path movingPath = null;
  	private Path crossPath = null;
  	private Path trianglePath = null;
  
  	// scale bar
  	private int scaleBarMeters;
  	private int scaleBarWidth;
  	private Paint scaleLinePaint;
  	private Paint scaleTextPaint;
  	private Paint scaleFillPaint;
  	private boolean drawScaleBackground;
  	private long lastScaleMove = 0;
  	private int lastScalePos = 1;
  
  	private Androzic application;
  	private MapHolder mapHolder;
  
  	private SurfaceHolder cachedHolder;
  	private DrawingThread drawingThread;
  
  	private MultiTouchController<Object> multiTouchController;
  	private float pinch = 0;
  	private float scale = 1;
  	private boolean wasMultitouch = false;
  
  	private Viewport currentViewport;
  	
  	private float density = 1f;
  	
  	private boolean recreateBuffers;
  	private Bitmap bufferBitmap;
  	private Bitmap bufferBitmapTmp;
  	private Handler renderHandler;
  	private Viewport renderViewport;
  
  	public MapView(Context context)
  	{
  		super(context);
  		setWillNotDraw(false);
  	}
  
  	public MapView(Context context, AttributeSet attrs)
  	{
  		super(context, attrs);
  		setWillNotDraw(false);
  	}
  
  	public MapView(Context context, AttributeSet attrs, int defStyle)
  	{
  		super(context, attrs, defStyle);
  		setWillNotDraw(false);
  	}
  
  	public void initialize(Androzic application, MapHolder holder)
  	{
  		this.application = application;
  		this.mapHolder = holder;
  
  		currentViewport = new Viewport();
  		renderHandler = new Handler(application.getRenderingThreadLooper());
  		recreateBuffers = false;
  
  		getHolder().addCallback(this);
  
  		Resources resources = getResources();
  		density = resources.getDisplayMetrics().density;
  
  		scaleLinePaint = new Paint();
  		scaleLinePaint.setAntiAlias(false);
  		scaleLinePaint.setStrokeWidth(density);
  		scaleLinePaint.setStyle(Paint.Style.STROKE);
  		scaleLinePaint.setColor(resources.getColor(R.color.scalebar));
          scaleTextPaint = new Paint();
          scaleTextPaint.setAntiAlias(true);
          scaleTextPaint.setStrokeWidth(density);
          scaleTextPaint.setStyle(Paint.Style.FILL);
          scaleTextPaint.setTextAlign(Align.CENTER);
          scaleTextPaint.setTextSize(10 * density);
          scaleTextPaint.setTypeface(Typeface.SANS_SERIF);
          scaleTextPaint.setColor(resources.getColor(R.color.scalebar));
  		scaleFillPaint = new Paint();
  		scaleFillPaint.setAntiAlias(false);
  		scaleFillPaint.setStrokeWidth(density);
  		scaleFillPaint.setStyle(Paint.Style.FILL_AND_STROKE);
  		scaleFillPaint.setColor(resources.getColor(R.color.scalebarbg));
  
  		drawScaleBackground = true;
  		
      	lastScaleMove = 0;
      	lastScalePos = 1;
  
  		crossPaint = new Paint();
  		crossPaint.setAntiAlias(true);
  		crossPaint.setStrokeWidth(density);
  		crossPaint.setStyle(Paint.Style.STROKE);
  		crossPaint.setColor(resources.getColor(R.color.mapcross));
  
  		pointerPaint = new Paint();
  		pointerPaint.setAntiAlias(true);
  		pointerPaint.setStrokeWidth(2 * density);
  		pointerPaint.setStyle(Paint.Style.STROKE);
  		pointerPaint.setColor(resources.getColor(R.color.cursor));
  
  		compassPaint = new Paint();
  		compassPaint.setAntiAlias(true);
  		compassPaint.setStrokeWidth(2 * density);
  		compassPaint.setStyle(Paint.Style.STROKE);
  		compassPaint.setColor(resources.getColor(R.color.north));
  
  		crossPath = new Path();
  		crossPath.addCircle(0, 0, 1 * density, Path.Direction.CW);
  		crossPath.addCircle(0, 0, 40 * density, Path.Direction.CW);
  		crossPath.moveTo(20 * density, 0);
  		crossPath.lineTo(100 * density, 0);
  		crossPath.moveTo(-20 * density, 0);
  		crossPath.lineTo(-100 * density, 0);
  		crossPath.moveTo(0, 20 * density);
  		crossPath.lineTo(0, 100 * density);
  		crossPath.moveTo(0, -20 * density);
  		crossPath.lineTo(0, -100 * density);
  		
  		trianglePath = new Path();
  		trianglePath.moveTo(0, -55 * density);
  		trianglePath.lineTo(5 * density, -45 * density);
  		trianglePath.lineTo(-5 * density, -45 * density);
  		trianglePath.lineTo(0, -55 * density);
  
  		movingPath = new Path();
  		movingPath.moveTo(0, 0);
  		movingPath.lineTo(10 * density, 12 * density);
  		movingPath.lineTo(3 * density, 10 * density);
  		movingPath.lineTo(3 * density, 30 * density);
  		movingPath.lineTo(-3 * density, 30 * density);
  		movingPath.lineTo(-3 * density, 10 * density);
  		movingPath.lineTo(-10 * density, 12 * density);
  		movingPath.lineTo(0, 0);
  		
  		if (application.customCursor != null)
  		{
  			movingCursor = application.customCursor;
  			movingCursor.setBounds(-movingCursor.getIntrinsicWidth() / 2, 0, movingCursor.getIntrinsicWidth() / 2, movingCursor.getIntrinsicHeight());
  		}
  
  		multiTouchController = new MultiTouchController<>(this, false);
  		tapHandler = new GestureHandler(this);
  
  		Log.d(TAG, "Map initialize");
  	}
  
  	@Override
  	public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
  	{
  		Log.e(TAG, "surfaceChanged(" + width + "," + height + ")");
  		currentViewport.width = width;
  		currentViewport.height = height;
  		calculateViewportCanvas();
  		calculateViewportBounds();
  		calculateScaleBar();
  		setLookAhead(lookAheadPst);
  		recreateBuffers = true;
  		refreshBuffer();
  	}
  
  	@Override
  	public void surfaceCreated(SurfaceHolder holder)
  	{
  		Log.e(TAG, "surfaceCreated(" + holder + ")");
  		currentViewport.width = getWidth();
  		currentViewport.height = getHeight();
  		calculateViewportCanvas();
  		calculateViewportBounds();
  		calculateScaleBar();
  		setLookAhead(lookAheadPst);
  		recreateBuffers = true;
  		refreshBuffer();
  		
  		lastDragTime = SystemClock.uptimeMillis();
  
  		drawingThread = new DrawingThread(holder, this);
  		drawingThread.setRunning(true);
  		drawingThread.start();
  		cachedHolder = null;
  	}
  
  	@Override
  	public void surfaceDestroyed(SurfaceHolder holder)
  	{
  		Log.d(TAG, "surfaceDestroyed(" + holder + ")");
  		boolean retry = true;
  		drawingThread.setRunning(false);
  		while (retry)
  		{
  			try
  			{
  				drawingThread.join();
  				retry = false;
  			}
  			catch (InterruptedException e)
  			{
  				//ignore
  			}
  		}
  		if (bufferBitmap != null)
  			bufferBitmap.recycle();
  		if (bufferBitmapTmp != null)
  			bufferBitmapTmp.recycle();
  		bufferBitmap = null;
  		bufferBitmapTmp = null;
  	}
  
  	/**
  	 * Pauses map drawing
  	 */
  	public void pause()
  	{
  		if (cachedHolder != null || drawingThread == null)
  			return;
  		cachedHolder = drawingThread.surfaceHolder;
  		surfaceDestroyed(cachedHolder);
  	}
  
  	/**
  	 * Resumes map drawing
  	 */
  	public void resume()
  	{
  		if (cachedHolder != null)
  			surfaceCreated(cachedHolder);
  	}
  
  	/**
  	 * Checks if map drawing is paused
  	 */
  	@SuppressWarnings("unused")
  	public boolean isPaused()
  	{
  		return cachedHolder != null;
  	}
  
  	class DrawingThread extends Thread
  	{
  		private boolean runFlag = false;
  		private SurfaceHolder surfaceHolder;
  		private MapView mapView;
  		private long prevTime;
  
  		public DrawingThread(SurfaceHolder surfaceHolder, MapView mapView)
  		{
  			this.surfaceHolder = surfaceHolder;
  			this.mapView = mapView;
  			prevTime = SystemClock.uptimeMillis();
  		}
  
  		public void setRunning(boolean run)
  		{
  			runFlag = run;
  		}
  
  		@Override
  		public void run()
  		{
  			Canvas canvas;
  			while (runFlag)
  			{
  				// limit the frame rate to maximum 5 frames per second (200 milliseconds)
  				long elapsedTime = SystemClock.uptimeMillis() - prevTime;
  				if (elapsedTime < drawPeriod)
  				{
  					try
  					{
  						Thread.sleep(drawPeriod - elapsedTime);
  					}
  					catch (InterruptedException e)
  					{
  						//ignore
  					}
  				}
  				prevTime = SystemClock.uptimeMillis();
  				canvas = null;
  				try
  				{
  					canvas = surfaceHolder.lockCanvas();
  					drawPeriod = mapView.calculateLookAhead() ? 50 : 200;
  					if (canvas != null)
  						mapView.doDraw(canvas);
  				}
  				finally
  				{
  					if (canvas != null)
  					{
  						surfaceHolder.unlockCanvasAndPost(canvas);
  					}
  				}
  			}
  		}
  	}
  
  	protected void doDraw(Canvas canvas)
  	{
  		long now = SystemClock.uptimeMillis();
  
  		Matrix matrix = new Matrix();
  		matrix.postTranslate((currentViewport.width - currentViewport.canvasWidth) / 2, (currentViewport.height - currentViewport.canvasHeight) / 2);
  		
  		boolean scaled = scale > 1.1 || scale < 0.9;
  		if (scaled)
  		{
  			float dx = currentViewport.canvasWidth * (1 - scale) / 2;
  			float dy = currentViewport.canvasWidth * (1 - scale) / 2;
  			canvas.translate(dx, dy);
  			matrix.postScale(scale, scale);
  		}
  		
  		canvas.drawARGB(255, 255, 255, 255);
  		
  		synchronized (this)
  		{
  			if (bufferBitmap != null && !bufferBitmap.isRecycled())
  			{
  				// Difference between current and buffer map center
  				int mcXdiff = renderViewport.mapCenterXY[0] - currentViewport.mapCenterXY[0];
  				int mcYdiff = renderViewport.mapCenterXY[1] - currentViewport.mapCenterXY[1];
  				// Difference between current and buffer look ahead
  				int laXdiff = renderViewport.lookAheadXY[0] - currentViewport.lookAheadXY[0];
  				int laYdiff = renderViewport.lookAheadXY[1] - currentViewport.lookAheadXY[1];
  				// Adjust buffer bitmap position
  				matrix.postTranslate(mcXdiff - laXdiff, mcYdiff - laYdiff);
  				// Draw buffer bitmap
  				canvas.drawBitmap(bufferBitmap, matrix, null);
  			}
  		}
  
  		if (scaled)
  			return;
  
  		int cx = currentViewport.width / 2;
  		int cy = currentViewport.height / 2;
  
  		//canvas.translate(-currentViewport.width + currentViewport.canvasWidth, -currentViewport.height + currentViewport.canvasHeight);
  
  		// Draw scale bar
  		if (mpp > 0)
  		{
  			int t = 10000;
  			if (scaleBarMeters <= t && scaleBarMeters * 2 > t)
  				t = scaleBarMeters * 3;
  			String[] d = StringFormatter.distanceC(scaleBarMeters, t);
  			String d2 = StringFormatter.distanceH(scaleBarMeters*2, t);
  
  			Rect rect = new Rect();
  			scaleTextPaint.getTextBounds(d[0], 0, d[0].length(), rect);
  			int htw = rect.width() / 2;
  			int th = rect.height();
  			scaleTextPaint.getTextBounds(d2, 0, d2.length(), rect);
  			int httw = rect.width() / 2;
  
  			final int x2 = scaleBarWidth * 2;
  			final int x3 = scaleBarWidth * 3;
  			final int xd2 = scaleBarWidth / 2;
  			final int xd4 = scaleBarWidth / 4;
  			
  			int dp3 = (int) (3 * density);
  			int dp6 = (int) (6 * density);
  
  			int scaleX = 0;
  			int scaleY = 0;
  			int cty;
  	
  			int pos;
  			if (mapRotate || !isFollowing)
  				pos = 1;
  			else if (currentViewport.bearing >= 0 && currentViewport.bearing < 90)
  				pos = 1;
  			else if (currentViewport.bearing >= 90 && currentViewport.bearing < 180)
  				pos = 2;
  			else if (currentViewport.bearing >= 180 && currentViewport.bearing < 270)
  				pos = 3;
  			else
  				pos = 4;
  	
  			if (pos != lastScalePos)
  			{
  				if (lastScaleMove == 0)
  				{
  					pos = lastScalePos;
  					lastScaleMove = now;
  				}
  				else if (now > lastScaleMove + SCALE_MOVE_DELAY)
  				{
  					lastScalePos = pos;
  					lastScaleMove = 0;
  				}
  				else
  				{
  					pos = lastScalePos;
  				}
  			}
  	
  			if (pos == 1)
  			{
  				scaleX += currentViewport.viewArea.left + htw + dp6;
  				scaleY += currentViewport.viewArea.bottom - dp6 * 2;
  				cty = -dp6;
  			}
  			else if (pos == 2)
  			{
  				scaleX += currentViewport.viewArea.left + htw + dp6;
  				scaleY += currentViewport.viewArea.top + dp6;
  				cty = th + dp6 + dp3;
  			}
  			else if (pos == 3)
  			{
  				scaleX += currentViewport.viewArea.right - x3 - httw - dp6;
  				scaleY += currentViewport.viewArea.top + dp6;
  				cty = th + dp6 + dp3;
  			}
  			else
  			{
  				scaleX += currentViewport.viewArea.right - x3 - httw - dp6;
  				scaleY += currentViewport.viewArea.bottom - dp6 * 2;
  				cty = -dp6;
  			}
  	
  			if (drawScaleBackground)
  			{
  				int bt = cty > 0 ? scaleY : scaleY + cty - th;
  				int bb = cty > 0 ? scaleY + cty : scaleY + dp6;
  				rect = new Rect(scaleX-htw, bt, scaleX+x3+httw, bb);
  				rect.inset(-2, -2);
  				canvas.drawRect(rect, scaleFillPaint);
  			}
  	
  			canvas.drawLine(scaleX, scaleY, scaleX+x3, scaleY, scaleLinePaint);
  			canvas.drawLine(scaleX, scaleY+dp6, scaleX+x3, scaleY+dp6, scaleLinePaint);
  			canvas.drawLine(scaleX, scaleY, scaleX, scaleY+dp6, scaleLinePaint);
  			canvas.drawLine(scaleX+x3, scaleY, scaleX+x3, scaleY+dp6, scaleLinePaint);
  			canvas.drawLine(scaleX+scaleBarWidth, scaleY, scaleX+scaleBarWidth, scaleY+dp6, scaleLinePaint);
  			canvas.drawLine(scaleX+x2, scaleY, scaleX+x2, scaleY+dp6, scaleLinePaint);
  			canvas.drawLine(scaleX+scaleBarWidth, scaleY+dp3, scaleX+x2, scaleY+dp3, scaleLinePaint);
  			canvas.drawLine(scaleX, scaleY+dp3, scaleX+xd4, scaleY+dp3, scaleLinePaint);
  			canvas.drawLine(scaleX+xd2, scaleY+dp3, scaleX+xd2+xd4, scaleY+dp3, scaleLinePaint);
  			canvas.drawLine(scaleX+xd4, scaleY, scaleX+xd4, scaleY+dp6, scaleLinePaint);
  			canvas.drawLine(scaleX+xd2, scaleY, scaleX+xd2, scaleY+dp6, scaleLinePaint);
  			canvas.drawLine(scaleX+xd2+xd4, scaleY, scaleX+xd2+xd4, scaleY+dp6, scaleLinePaint);
  	
  			canvas.drawText("0", scaleX+scaleBarWidth, scaleY+cty, scaleTextPaint);
  			canvas.drawText(d[0], scaleX+x2, scaleY+cty, scaleTextPaint);
  			canvas.drawText(d[0], scaleX, scaleY+cty, scaleTextPaint);
  			canvas.drawText(d2, scaleX+x3, scaleY+cty, scaleTextPaint);
  		}
  
  		canvas.translate(currentViewport.lookAheadXY[0] + cx, currentViewport.lookAheadXY[1] + cy);
  
  		boolean showCross = now < lastDragTime + crossCursorHideDelay;
  
  		// Draw north triangle
  		if (mapRotate && isFollowing)
  		{
  			canvas.save();
  			canvas.rotate(-renderViewport.mapHeading, 0, 0);
  			canvas.drawPath(trianglePath, compassPaint);
  			canvas.restore();
  		}
  
  		// draw cursor (it is always topmost)
  		if (isMoving)
  		{
  			int sx = currentViewport.locationXY[0] - currentViewport.mapCenterXY[0];
  			int sy = currentViewport.locationXY[1] - currentViewport.mapCenterXY[1];
  
  			canvas.save();
  			canvas.translate(sx, sy);
  			canvas.rotate(currentViewport.bearing - renderViewport.mapHeading, 0, 0);
  			if (movingCursor != null)
  				movingCursor.draw(canvas);
  			else
  				canvas.drawPath(movingPath, pointerPaint);
  			if (isFixed)
  				canvas.drawLine(0, 0, 0, -vectorLength, pointerPaint);
  			canvas.restore();
  
  			sx += cx;
  			sy += cy;
  
  			// Draw overflow bearing triangle
  			if (showCross && (sx < 0 || sy < 0 || sx > currentViewport.width || sy > currentViewport.height))
  			{
  				canvas.save();
  				double bearing = Geo.bearing(currentViewport.mapCenter[0], currentViewport.mapCenter[1], currentViewport.location[0], currentViewport.location[1]);
  				canvas.rotate((float) bearing, 0, 0);
  				canvas.drawPath(trianglePath, pointerPaint);
  				canvas.restore();
  			}
  		}
  
  		// Draw map center cross
  		if (!isFollowing && showCross)
  			canvas.drawPath(crossPath, crossPaint);
  	}
  	
  	public void refreshMap()
  	{
  		refreshBuffer();
  	}
  	
  	private void refreshBuffer()
  	{
  		if (!renderHandler.hasMessages(REFRESH_MESSAGE))
  		{
  			Message msg = Message.obtain(renderHandler, new Runnable() {
  				@Override
  				public void run()
  				{
  					refreshBufferInternal();
  				}
  			});
  			msg.what = REFRESH_MESSAGE;
  			renderHandler.sendMessage(msg);
  		}
  	}
  
  	private void refreshBufferInternal()
  	{
  		Log.d(TAG, "refreshBufferInternal("+currentViewport.canvasWidth+","+currentViewport.canvasHeight+")");
  
  		if (currentViewport.canvasWidth == 0 || currentViewport.canvasHeight == 0)
  			return;
  
  		boolean recreatedBuffer = false;
  
  		if (recreateBuffers || bufferBitmapTmp == null || bufferBitmapTmp.isRecycled())
  		{
  			synchronized (this)
  			{
  				if (bufferBitmapTmp != null)
  					bufferBitmapTmp.recycle();
  				bufferBitmapTmp = Bitmap.createBitmap(currentViewport.canvasWidth, currentViewport.canvasHeight, Bitmap.Config.RGB_565);
  				if (recreateBuffers)
  				{
  					recreatedBuffer = true;
  					recreateBuffers = false;
  				}
  			}
  		}
  		
  		Canvas canvas = new Canvas(bufferBitmapTmp);
  		Viewport viewport = currentViewport.copy();
  		
  		canvas.drawRGB(0xFF, 0xFF, 0xFF);
  
  		int cx = viewport.canvasWidth / 2;
  		int cy = viewport.canvasHeight / 2;
  
  		if (mapRotate && isFollowing)
  			canvas.rotate(-viewport.mapHeading, viewport.lookAheadXY[0] + cx, viewport.lookAheadXY[1] + cy);
  
  		application.drawMap(viewport, loadBestMap, canvas);
  
  		canvas.translate(viewport.lookAheadXY[0] + cx, viewport.lookAheadXY[1] + cy);
  
  		// draw overlays
  		// FIXME Optimize getOverlays()
  		for (MapOverlay mo : application.overlayManager.getOverlays(OverlayManager.ORDER_DRAW_PREFERENCE))
  			if (mo.isEnabled())
  				mo.onPrepareBuffer(viewport, canvas);
  		for (MapOverlay mo : application.overlayManager.getOverlays(OverlayManager.ORDER_DRAW_PREFERENCE))
  			if (mo.isEnabled())
  				mo.onPrepareBufferEx(viewport, canvas);
  
  		synchronized (this)
  		{
  			Bitmap t = bufferBitmap;
  			renderViewport = viewport;
  			bufferBitmap = bufferBitmapTmp;
  			bufferBitmapTmp = t;
  			
  			if (recreatedBuffer)
  			{
  				if (bufferBitmapTmp != null)
  					bufferBitmapTmp.recycle();
  				bufferBitmapTmp = null;
  			}
  		}
  	}
  
  	public void setLocation(Location loc)
  	{
  		currentViewport.bearing = loc.getBearing();
  		currentViewport.speed = loc.getSpeed();
  
  		currentViewport.location[0] = loc.getLatitude();
  		currentViewport.location[1] = loc.getLongitude();
  		application.getXYbyLatLon(currentViewport.location[0], currentViewport.location[1], currentViewport.locationXY);
  
  		float turn = lookAheadB - currentViewport.bearing;
  		if (Math.abs(turn) > 180)
  		{
  			turn = turn - Math.signum(turn) * 360;
  		}
  		if (Math.abs(turn) > 10)
  			lookAheadB = currentViewport.bearing;
  
  		if (mapRotate && isFollowing)
  		{
  			turn = currentViewport.mapHeading - currentViewport.bearing;
  			if (Math.abs(turn) > 180)
  			{
  				turn = turn - Math.signum(turn) * 360;
  			}
  			if (Math.abs(turn) > 10)
  			{
  				currentViewport.mapHeading = currentViewport.bearing;
  				refreshBuffer();
  			}
  
  			lookAheadB = 0;
  		}
  
  		long lastLocationMillis = loc.getTime();
  
  		if (isFollowing)
  		{
  			boolean newMap;
  			if (bestMapEnabled && bestMapInterval > 0 && lastLocationMillis - lastBestMap >= bestMapInterval)
  			{
  				newMap = application.setMapCenter(currentViewport.location[0], currentViewport.location[1], true, false, loadBestMap);
  				lastBestMap = lastLocationMillis;
  			}
  			else
  			{
  				newMap = application.setMapCenter(currentViewport.location[0], currentViewport.location[1], true, false, false);
  				if (newMap)
  					loadBestMap = bestMapEnabled;
  			}
  			if (newMap)
  				updateMapInfo();
  			updateMapCenter();
  		}
  		calculateVectorLength();
  	}
  
  	/**
  	 * Clears current location from map.
  	 */
  	public void clearLocation()
  	{
  		setFollowingThroughContext(false);
  		currentViewport.location[0] = Double.NaN;
  		currentViewport.location[1] = Double.NaN;
  		currentViewport.bearing = 0;
  		currentViewport.speed = 0;
  		calculateVectorLength();
  	}
  
  	public void updateMapInfo()
  	{
  		Log.d(TAG, "updateMapInfo()");
  		scale = 1;
  		BaseMap map = application.getCurrentMap();
  		if (map == null)
  			mpp = 0;
  		else
  			mpp = map.getMPP();
  		if (!Double.isNaN(currentViewport.location[0]))
  			application.getXYbyLatLon(currentViewport.location[0], currentViewport.location[1], currentViewport.locationXY);
  		calculateVectorLength();
  		calculateScaleBar();
  		application.overlayManager.notifyOverlays();
  		try
  		{
  			mapHolder.updateFileInfo();
  		}
  		catch (Exception e)
  		{
  			//ignore
  		}
  	}
  
  	public Viewport getViewport()
  	{
  		return currentViewport.copy();
  	}
  
  	private void calculateViewportCanvas()
  	{
  		int excess = VIEWPORT_EXCESS * 2;
  		if (mapRotate)
  		{
  			int a = currentViewport.width < currentViewport.height ? currentViewport.height : currentViewport.width;
  			int e = (int) (0.41421356237 * a);
  			if (e < excess)
  				e = excess;
  			currentViewport.canvasWidth = a + e;
  			currentViewport.canvasHeight = a + e;
  		}
  		else
  		{
  			currentViewport.canvasWidth = currentViewport.width + excess;
  			currentViewport.canvasHeight = currentViewport.height + excess;
  		}
  		application.updateViewportDimensions(currentViewport.canvasWidth, currentViewport.canvasHeight);
  	}
  
  	private void calculateViewportBounds()
  	{
  		int cx = currentViewport.width / 2;
  		int cy = currentViewport.height / 2;
  		double[] ll = new double[2];
  		Bounds area = new Bounds();
  		application.getLatLonByXY(currentViewport.mapCenterXY[0] - cx, currentViewport.mapCenterXY[1] - cy, ll);
  		area.extend(ll[0], ll[1]);
  		application.getLatLonByXY(currentViewport.mapCenterXY[0] + cx, currentViewport.mapCenterXY[1] - cy, ll);
  		area.extend(ll[0], ll[1]);
  		application.getLatLonByXY(currentViewport.mapCenterXY[0] - cx, currentViewport.mapCenterXY[1] + cy, ll);
  		area.extend(ll[0], ll[1]);
  		application.getLatLonByXY(currentViewport.mapCenterXY[0] + cx, currentViewport.mapCenterXY[1] + cy, ll);
  		area.extend(ll[0], ll[1]);
  		currentViewport.mapArea = area;
  	}
  
  	/**
  	 * 
  	 * @return True if look ahead position was recalculated
  	 */
  	private boolean calculateLookAhead()
  	{
  		boolean recalculated = false;
  		if (lookAheadC != lookAheadS)
  		{
  			recalculated = true;
  
  			float diff = lookAheadC - lookAheadS;
  			if (Math.abs(diff) > Math.abs(lookAheadSS) * (MAX_SHIFT_SPEED / INC_SHIFT_SPEED))
  			{
  				lookAheadSS += Math.signum(diff) * INC_SHIFT_SPEED;
  				if (Math.abs(lookAheadSS) > MAX_SHIFT_SPEED)
  				{
  					lookAheadSS = Math.signum(lookAheadSS) * MAX_SHIFT_SPEED;
  				}
  			}
  			else if (Math.signum(diff) != Math.signum(lookAheadSS))
  			{
  				lookAheadSS += Math.signum(diff) * INC_SHIFT_SPEED * 2;
  			}
  			else if (Math.abs(lookAheadSS) > INC_SHIFT_SPEED)
  			{
  				lookAheadSS -= Math.signum(diff) * INC_SHIFT_SPEED * 0.5;
  			}
  			if (Math.abs(diff) < INC_SHIFT_SPEED)
  			{
  				lookAheadS = lookAheadC;
  				lookAheadSS = 0;
  			}
  			else
  			{
  				lookAheadS += lookAheadSS;
  			}
  		}
  		if (lookAheadC > 0 && lookAheadB != smoothB)
  		{
  			recalculated = true;
  
  			float turn = lookAheadB - smoothB;
  			if (Math.abs(turn) > 180)
  			{
  				turn = turn - Math.signum(turn) * 360;
  			}
  			if (Math.abs(turn) > Math.abs(smoothBS) * (MAX_ROTATION_SPEED / INC_ROTATION_SPEED))
  			{
  				smoothBS += Math.signum(turn) * INC_ROTATION_SPEED;
  				if (Math.abs(smoothBS) > MAX_ROTATION_SPEED)
  				{
  					smoothBS = Math.signum(smoothBS) * MAX_ROTATION_SPEED;
  				}
  			}
  			else if (Math.signum(turn) != Math.signum(smoothBS))
  			{
  				smoothBS += Math.signum(turn) * INC_ROTATION_SPEED * 2;
  			}
  			else if (Math.abs(smoothBS) > INC_ROTATION_SPEED)
  			{
  				smoothBS -= Math.signum(turn) * INC_ROTATION_SPEED * 0.5;
  			}
  			if (Math.abs(turn) < INC_ROTATION_SPEED)
  			{
  				smoothB = lookAheadB;
  				smoothBS = 0;
  			}
  			else
  			{
  				smoothB += smoothBS;
  				if (smoothB >= 360)
  					smoothB -= 360;
  				if (smoothB < 0)
  					smoothB = 360 - smoothB;
  			}
  		}
  		if (recalculated)
  		{
  			currentViewport.lookAheadXY[0] = (int) Math.round(Math.sin(Math.toRadians(smoothB)) * -lookAheadS);
  			currentViewport.lookAheadXY[1] = (int) Math.round(Math.cos(Math.toRadians(smoothB)) * lookAheadS);
  			refreshBuffer();
  		}
  		return recalculated;
  	}
  
  	private void calculateVectorLength()
  	{
  		if (mpp == 0)
  		{
  			vectorLength = 0;
  			return;
  		}
  		switch (vectorType)
  		{
  			case 0:
  				vectorLength = 0;
  				break;
  			case 1:
  				vectorLength = (int) (proximity / mpp);
  				break;
  			case 2:
  				vectorLength = (int) (currentViewport.speed * 60 / mpp);
  		}
  		vectorLength *= vectorMultiplier;
  	}
  
  	private void calculateScaleBar()
  	{
  		if (mpp == 0)
  			return;
  
  		int w = currentViewport.viewArea.right - currentViewport.viewArea.left;
  		int h = currentViewport.viewArea.bottom - currentViewport.viewArea.top;
  		int d = w > h ? 8 : 6;
  		
  		scaleBarMeters = (int) (mpp * w / d);
  		Log.e(TAG, "Scale bar: " + scaleBarMeters);
  		if (scaleBarMeters == 0)
  			scaleBarMeters = 1;
  		else //noinspection StatementWithEmptyBody
  			if (scaleBarMeters < 10) {}
  		else if (scaleBarMeters < 40)
  			scaleBarMeters = scaleBarMeters / 10 * 10;
  		else if (scaleBarMeters < 80)
  			scaleBarMeters = 50;
  		else if (scaleBarMeters < 130)
  			scaleBarMeters = 100;
  		else if (scaleBarMeters < 300)
  			scaleBarMeters = 200;
  		else if (scaleBarMeters < 700)
  			scaleBarMeters = 500;
  		else if (scaleBarMeters < 900)
  			scaleBarMeters = 800;
  		else if (scaleBarMeters < 1300)
  			scaleBarMeters = 1000;
  		else if (scaleBarMeters < 3000)
  			scaleBarMeters = 2000;
  		else if (scaleBarMeters < 7000)
  			scaleBarMeters = 5000;
  		else if (scaleBarMeters < 10000)
  			scaleBarMeters = 8000;
  		else if (scaleBarMeters < 80000)
  			scaleBarMeters = (int) (Math.ceil(scaleBarMeters * 1. / 10000) * 10000);
  		else
  			scaleBarMeters = (int) (Math.ceil(scaleBarMeters * 1. / 100000) * 100000);
  
  		scaleBarWidth = (int) (scaleBarMeters / mpp);
  
  		if (scaleBarWidth > currentViewport.width / 4)
  		{
  			scaleBarWidth /= 2;
  			scaleBarMeters /= 2;
  		}
  	}
  
  	public void setMoving(boolean moving)
  	{
  		isMoving = moving;
  		setLookAhead();
  	}
  
  	public boolean isMoving()
  	{
  		return isMoving;
  	}
  
  	public void setFollowing(boolean follow)
  	{
  		if (Double.isNaN(currentViewport.location[0]))
  			return;
  
  		if (isFollowing != follow)
  		{
  			if (follow)
  			{
  				Toast.makeText(getContext(), R.string.following_enabled, Toast.LENGTH_SHORT).show();
  				
  				boolean newMap = application.setMapCenter(currentViewport.location[0], currentViewport.location[1], true, true, false);
  				if (newMap)
  					updateMapInfo();
  				
  				int dx = currentViewport.locationXY[0] - currentViewport.mapCenterXY[0];
  				int dy = currentViewport.locationXY[1] - currentViewport.mapCenterXY[1];
  				int sx = dx + currentViewport.width / 2;
  				int sy = dy + currentViewport.height / 2;
  				
  				if (sx >= 0 && sy >= 0 && sx <= currentViewport.width && sy <= currentViewport.height)
  				{
  					// Location is inside current viewport
  					lookAheadS = (float) Math.sqrt(dx * dx + dy * dy);
  					smoothB = dx == 0 ? 0f : dy == 0 ? Math.signum(dx) * 90 : (float) Math.toDegrees(Math.atan(1. * dx / dy));
  
  					if (dy < 0)
  						smoothB = 180 - smoothB;
  					if (dy > 0)
  						smoothB = -smoothB;
  					if (smoothB < 0)
  						smoothB = 360 + smoothB;
  
  					currentViewport.lookAheadXY[0] = dx;
  					currentViewport.lookAheadXY[1] = dy;
  				}
  				if (mapRotate)
  				{
  					currentViewport.mapHeading = currentViewport.bearing;
  					refreshBuffer();
  				}
  			}
  			else
  			{
  				currentViewport.mapHeading = 0f;
  				refreshBuffer();
  				Toast.makeText(getContext(), R.string.following_disabled, Toast.LENGTH_SHORT).show();
  				boolean mapChanged = application.scrollMap(-currentViewport.lookAheadXY[0], -currentViewport.lookAheadXY[1], true);
  				if (mapChanged)
  					updateMapInfo();
  			}
  			updateMapCenter();
  			isFollowing = follow;
  			setLookAhead();
  		}
  	}
  
  	private void setFollowingThroughContext(boolean follow)
  	{
  		if (isFollowing != follow)
  		{
  			try
  			{
  				mapHolder.setFollowing(!isFollowing);
  			}
  			catch (Exception e)
  			{
  				setFollowing(false);
  			}
  		}
  	}
  
  	public boolean isFollowing()
  	{
  		return isFollowing;
  	}
  
  	public void setStrictUnfollow(boolean mode)
  	{
  		strictUnfollow = mode;
  	}
  
  	public boolean getStrictUnfollow()
  	{
  		return strictUnfollow;
  	}
  
  	public void setBestMapEnabled(boolean best)
  	{
  		bestMapEnabled = best;
  	}
  
  	public void suspendBestMap()
  	{
  		loadBestMap = false;
  	}
  
  	@SuppressWarnings("unused")
  	public boolean isBestMapEnabled()
  	{
  		return loadBestMap;
  	}
  
  	public void setBestMapInterval(int best)
  	{
  		bestMapInterval = best;
  	}
  
  	public void setFixed(boolean fixed)
  	{
  		isFixed = fixed;
  		if (movingCursor != null)
  			movingCursor.setColorFilter(isFixed ? active : null);
  		else
  			pointerPaint.setColor(isFixed ? activeColor : Color.GRAY);
  		lastDragTime = SystemClock.uptimeMillis();
  		setLookAhead();
  	}
  
  	public boolean isFixed()
  	{
  		return isFixed;
  	}
  
  	/**
  	 * Set the amount of screen intended for looking ahead
  	 * 
  	 * @param ahead
  	 *            % of the smaller dimension of available view area
  	 */
  	public void setLookAhead(final int ahead)
  	{
  		lookAheadPst = ahead;
  		final int w = currentViewport.viewArea.width();
  		final int h = currentViewport.viewArea.height();
  		final int half = w > h ? h / 2 : w / 2;
  		lookAhead = (int) (half * ahead * 0.01);
  		setLookAhead();
  	}
  
  	/**
  	 * Set current look ahead amount based on map conditions
  	 */
  	private void setLookAhead()
  	{
  		if (isMoving && isFollowing && isFixed)
  		{
  			lookAheadC = lookAhead;
  		}
  		else
  		{
  			lookAheadC = 0;
  			lookAheadS = 0;
  			lookAheadSS = 0;
  			currentViewport.lookAheadXY[0] = 0;
  			currentViewport.lookAheadXY[1] = 0;
  		}
  	}
  
  	public void setMapRotation(final int rotation)
  	{
  		mapRotate = rotation > 0;
  		calculateViewportCanvas();
  		currentViewport.mapHeading = 0;
  		recreateBuffers = true;
  		refreshBuffer();
  	}
  
  	public void setScaleBarColor(final int color)
  	{
  		scaleLinePaint.setColor(color);
  		scaleTextPaint.setColor(color);
  	}
  	
  	public void setScaleBarBackgroundColor(final int color)
  	{
  		scaleFillPaint.setColor(color);
  	}
  		
  	public void setDrawScaleBarBackground(final boolean draw)
  	{
  		drawScaleBackground = draw;
  	}
  
  	public void setCrossCursorHideDelay(final int delay)
  	{
  		crossCursorHideDelay = delay;
  	}
  	public void setCrossColor(final int color)
  	{
  		crossPaint.setColor(color);
  	}
  
  	public void setCursorColor(final int color)
  	{
  		activeColor = color;
  		if (movingCursor != null)
  		{
  			active = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN);
  			movingCursor.setColorFilter(isFixed ? active : null);
  		}
  		pointerPaint.setColor(isFixed ? activeColor : Color.GRAY);
  	}
  
  	public void setCursorVector(final int type, final int multiplier)
  	{
  		vectorType = type;
  		vectorMultiplier = multiplier;
  	}
  
  	public void setProximity(final int proximity)
  	{
  		this.proximity = proximity;
  	}
  
  	public void updateViewArea(Rect area)
  	{
  		Log.d(TAG, "updateViewArea()");
  		currentViewport.viewArea.set(area);
  		setLookAhead(lookAheadPst);
  	}
  
  	public void updateMapCenter()
  	{
  		currentViewport.mapCenter = application.getMapCenter();
  		application.getXYbyLatLon(currentViewport.mapCenter[0], currentViewport.mapCenter[1], currentViewport.mapCenterXY);
  		calculateViewportBounds();
  		refreshBuffer();
  		
  		try
  		{
  			mapHolder.updateCoordinates(currentViewport.mapCenter);
  		}
  		catch (Exception e)
  		{
  			//ignore
  		}
  	}
  
  	private void onDrag(int deltaX, int deltaY)
  	{
  		if (currentViewport.mapHeading != 0f)
  		{
  			double rad = Math.toRadians(-currentViewport.mapHeading);
  			int dX = (int) (1. * deltaX * Math.cos(rad) + 1. * deltaY * Math.sin(rad));
  			int dY = (int) (1. * deltaX * Math.sin(-rad) + 1. * deltaY * Math.cos(rad));
  			deltaX = dX;
  			deltaY = dY;
  		}
  		lastDragTime = SystemClock.uptimeMillis();
  		application.scrollMap(-deltaX, -deltaY, false);
  		updateMapCenter();
  	}
  
  	private void onDragFinished(int deltaX, int deltaY)
  	{
  		if (currentViewport.mapHeading != 0f)
  		{
  			double rad = Math.toRadians(-currentViewport.mapHeading);
  			int dX = (int) (deltaX * Math.cos(rad) + deltaY * Math.sin(rad));
  			int dY = (int) (deltaX * Math.sin(-rad) + deltaY * Math.cos(rad));
  			deltaX = dX;
  			deltaY = dY;
  		}
  		lastDragTime = SystemClock.uptimeMillis();
  		mapHolder.mapTapped();
  		boolean mapChanged = application.scrollMap(-deltaX, -deltaY, true);
  		if (mapChanged)
  			updateMapInfo();
  		updateMapCenter();
  	}
  
  	private void onSingleTap(int x, int y)
  	{
  		x = x - getWidth() / 2;
  		y = y - getHeight() / 2;
  
  		if (isMoving && isFollowing && isFixed)
  		{
  			x -= currentViewport.lookAheadXY[0];
  			y -= currentViewport.lookAheadXY[1];
  		}
  
  		if (currentViewport.mapHeading != 0f)
  		{
  			double rad = Math.toRadians(-currentViewport.mapHeading);
  			int dX = (int) (1. * x * Math.cos(rad) + 1. * y * Math.sin(rad));
  			int dY = (int) (1. * x * Math.sin(-rad) + 1. * y * Math.cos(rad));
  			x = dX;
  			y = dY;
  		}
  
  		int mapTapX = x + currentViewport.mapCenterXY[0];
  		int mapTapY = y + currentViewport.mapCenterXY[1];
  
  		int dt = GESTURE_THRESHOLD_DP / 2;
  		Rect tap = new Rect(mapTapX - dt, mapTapY - dt, mapTapX + dt, mapTapY + dt);
  		for (MapOverlay mo : application.overlayManager.getOverlays(OverlayManager.ORDER_SHOW_PREFERENCE))
  			if (mo.onSingleTap(upEvent, tap, this))
  				break;
  	}
  
  	@SuppressWarnings("UnusedParameters")
  	private void onDoubleTap(int x, int y)
  	{
  		setFollowingThroughContext(!isFollowing);
  	}
  
  	private static final int TAP = 1;
  	private static final int CANCEL = 2;
  
  	private static class GestureHandler extends Handler
  	{
  		private final WeakReference<MapView> target;
  		
  		GestureHandler(MapView view)
  		{
  			super();
  			this.target = new WeakReference<>(view);
  		}
  
  		@Override
  		public void handleMessage(Message msg)
  		{
  			MapView mapView = target.get();
  			if (mapView == null)
  				return;
  			switch (msg.what)
  			{
  				case TAP:
  					mapView.onSingleTap(mapView.penOX, mapView.penOY);
  					mapView.cancelMotionEvent();
  					break;
  				case CANCEL:
  					mapView.cancelMotionEvent();
  					break;
  				default:
  					throw new RuntimeException("Unknown message " + msg); // never
  			}
  		}
  	}
  
  	private void cancelMotionEvent()
  	{
  		tapHandler.removeMessages(TAP);
  		tapHandler.removeMessages(CANCEL);
  		if (upEvent != null)
  			upEvent.recycle();
  		upEvent = null;
  		penX = 0;
  		penY = 0;
  		penOX = 0;
  		penOY = 0;
  		firstTapTime = 0;
  	}
  
  	@Override
  	public boolean onTouchEvent(@NonNull MotionEvent event)
  	{
  		if (multiTouchController.onTouchEvent(event))
  		{
  			wasMultitouch = true;
  			return true;
  		}
  
  		int action = event.getAction();
  
  		switch (action)
  		{
  			case MotionEvent.ACTION_DOWN:
  				boolean hadTapMessage = tapHandler.hasMessages(TAP);
  				if (hadTapMessage)
  					tapHandler.removeMessages(TAP);
  				tapHandler.removeMessages(CANCEL);
  
  				if (event.getEventTime() - firstTapTime <= DOUBLE_TAP_TIMEOUT)
  				{
  					onDoubleTap(penOX, penOY);
  					cancelMotionEvent();
  					wasDoubleTap = true;
  				}
  				else
  				{
  					firstTapTime = event.getDownTime();
  					lastDragTime = SystemClock.uptimeMillis();
  					mapHolder.mapTapped();
  				}
  
  				penOX = penX = (int) event.getX();
  				penOY = penY = (int) event.getY();
  				break;
  			case MotionEvent.ACTION_MOVE:
  				if (!wasMultitouch && (!isFollowing || !strictUnfollow))
  				{
  					int x = (int) event.getX();
  					int y = (int) event.getY();
  
  					int dx = -(penX - x);
  					int dy = -(penY - y);
  
  					if (!isFollowing && (Math.abs(dx) > 0 || Math.abs(dy) > 0))
  					{
  						penX = x;
  						penY = y;
  						onDrag(dx, dy);
  					}
  					if (Math.abs(dx) > GESTURE_THRESHOLD_DP || Math.abs(dy) > GESTURE_THRESHOLD_DP)
  					{
  						if (!strictUnfollow)
  							setFollowingThroughContext(false);
  					}
  				}
  				break;
  			case MotionEvent.ACTION_UP:
  				if (upEvent != null)
  					upEvent.recycle();
  				upEvent = MotionEvent.obtain(event);
  
  				int x = (int) event.getX();
  				int y = (int) event.getY();
  
  				int dx = -penOX + x;
  				int dy = -penOY + y;
  				if (!wasMultitouch && !wasDoubleTap && Math.abs(dx) < GESTURE_THRESHOLD_DP && Math.abs(dy) < GESTURE_THRESHOLD_DP)
  				{
  					tapHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
  				}
  				else if (wasMultitouch || wasDoubleTap)
  				{
  					wasMultitouch = false;
  					wasDoubleTap = false;
  					cancelMotionEvent();
  				}
  				else
  				{
  					onDragFinished(0, 0);
  					tapHandler.sendEmptyMessageDelayed(CANCEL, DOUBLE_TAP_TIMEOUT);
  				}
  				break;
  			case MotionEvent.ACTION_CANCEL:
  				wasMultitouch = false;
  				wasDoubleTap = false;
  				cancelMotionEvent();
  				break;
  		}
  
  		return true;
  	}
  
  	@Override
  	public boolean onKeyDown(int keyCode, @NonNull KeyEvent event)
  	{
  		switch (keyCode)
  		{
  			case KeyEvent.KEYCODE_DPAD_CENTER:
  				setFollowingThroughContext(!isFollowing);
  				return true;
  			case KeyEvent.KEYCODE_DPAD_DOWN:
  			case KeyEvent.KEYCODE_DPAD_UP:
  			case KeyEvent.KEYCODE_DPAD_LEFT:
  			case KeyEvent.KEYCODE_DPAD_RIGHT:
  				if (!isFollowing || !strictUnfollow)
  				{
  					int dx = 0;
  					int dy = 0;
  					switch (keyCode)
  					{
  						case KeyEvent.KEYCODE_DPAD_DOWN:
  							dy -= 10;
  							break;
  						case KeyEvent.KEYCODE_DPAD_UP:
  							dy += 10;
  							break;
  						case KeyEvent.KEYCODE_DPAD_LEFT:
  							dx += 10;
  							break;
  						case KeyEvent.KEYCODE_DPAD_RIGHT:
  							dx -= 10;
  							break;
  					}
  					if (isFollowing)
  						setFollowingThroughContext(false);
  					onDragFinished(dx, dy);
  					return true;
  				}
  		}
  
  		/*
  		 * for (MapOverlay mo : application.getOverlays()) if
  		 * (mo.onKeyDown(keyCode, event, this)) return true;
  		 */
  
  		return super.onKeyDown(keyCode, event);
  	}
  
  	@Override
  	public boolean onKeyUp(int keyCode, KeyEvent event)
  	{
  		/*
  		 * for (MapOverlay mo : application.getOverlays()) if
  		 * (mo.onKeyUp(keyCode, event, this)) return true;
  		 */
  
  		return super.onKeyUp(keyCode, event);
  	}
  
  	@Override
  	public boolean onTrackballEvent(MotionEvent event)
  	{
  		int action = event.getAction();
  		switch (action)
  		{
  			case MotionEvent.ACTION_UP:
  				setFollowingThroughContext(!isFollowing);
  				break;
  			case MotionEvent.ACTION_MOVE:
  				if (!isFollowing)
  				{
  					int n = event.getHistorySize();
  					final float scaleX = event.getXPrecision();
  					final float scaleY = event.getYPrecision();
  					int dx = (int) (-event.getX() * scaleX);
  					int dy = (int) (-event.getY() * scaleY);
  					for (int i = 0; i < n; i++)
  					{
  						dx += -event.getHistoricalX(i) * scaleX;
  						dy += -event.getHistoricalY(i) * scaleY;
  					}
  					if (Math.abs(dx) > 0 || Math.abs(dy) > 0)
  					{
  						onDragFinished(dx, dy);
  					}
  				}
  				break;
  		}
  
  		/*
  		 * for (MapOverlay mo : this.overlays) if (mo.onTrackballEvent(event,
  		 * this)) return true;
  		 */
  
  		return true;
  	}
  
  	@Override
  	protected void onRestoreInstanceState(Parcelable state)
  	{
  		if (state instanceof Bundle)
  		{
  			Bundle bundle = (Bundle) state;
  			super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
  
  			vectorType = bundle.getInt("vectorType");
  			vectorMultiplier = bundle.getInt("vectorMultiplier");
  			isFollowing = bundle.getBoolean("autoFollow");
  			strictUnfollow = bundle.getBoolean("strictUnfollow");
  			loadBestMap = bundle.getBoolean("loadBestMap");
  			bestMapInterval = bundle.getInt("bestMapInterval");
  
  			isFixed = bundle.getBoolean("isFixed");
  			isMoving = bundle.getBoolean("isMoving");
  			lastBestMap = bundle.getLong("lastBestMap");
  
  			penX = bundle.getInt("penX");
  			penY = bundle.getInt("penY");
  			penOX = bundle.getInt("penOX");
  			penOY = bundle.getInt("penOY");
  			currentViewport.lookAheadXY = bundle.getIntArray("lookAheadXY");
  			lookAhead = bundle.getInt("lookAhead");
  			lookAheadC = bundle.getFloat("lookAheadC");
  			lookAheadS = bundle.getFloat("lookAheadS");
  			lookAheadSS = bundle.getFloat("lookAheadSS");
  			lookAheadPst = bundle.getInt("lookAheadPst");
  			lookAheadB = bundle.getFloat("lookAheadB");
  			smoothB = bundle.getFloat("smoothB");
  			smoothBS = bundle.getFloat("smoothBS");
  
  			currentViewport.mapCenter = bundle.getDoubleArray("mapCenter");
  			currentViewport.location = bundle.getDoubleArray("currentLocation");
  			currentViewport.mapCenterXY = bundle.getIntArray("mapCenterXY");
  			currentViewport.locationXY = bundle.getIntArray("currentLocationXY");
  			currentViewport.mapHeading = bundle.getFloat("mapHeading");
  			currentViewport.bearing = bundle.getFloat("bearing");
  			currentViewport.speed = bundle.getFloat("speed");
  			mpp = bundle.getDouble("mpp");
  			vectorLength = bundle.getInt("vectorLength");
  			proximity = bundle.getInt("proximity");
  
  			// TODO Should be somewhere else?
  			if (movingCursor != null)
  				movingCursor.setColorFilter(isFixed ? active : null);
  		}
  		else
  		{
  			super.onRestoreInstanceState(state);
  		}
  	}
  
  	@Override
  	protected Parcelable onSaveInstanceState()
  	{
  		Bundle bundle = new Bundle();
  		bundle.putParcelable("instanceState", super.onSaveInstanceState());
  
  		bundle.putInt("vectorType", vectorType);
  		bundle.putInt("vectorMultiplier", vectorMultiplier);
  		bundle.putBoolean("autoFollow", isFollowing);
  		bundle.putBoolean("strictUnfollow", strictUnfollow);
  		bundle.putBoolean("loadBestMap", loadBestMap);
  		bundle.putInt("bestMapInterval", bestMapInterval);
  
  		bundle.putBoolean("isFixed", isFixed);
  		bundle.putBoolean("isMoving", isMoving);
  		bundle.putLong("lastBestMap", lastBestMap);
  
  		bundle.putInt("penX", penX);
  		bundle.putInt("penY", penY);
  		bundle.putInt("penOX", penOX);
  		bundle.putInt("penOY", penOY);
  		bundle.putIntArray("lookAheadXY", currentViewport.lookAheadXY);
  		bundle.putInt("lookAhead", lookAhead);
  		bundle.putFloat("lookAheadC", lookAheadC);
  		bundle.putFloat("lookAheadS", lookAheadS);
  		bundle.putFloat("lookAheadSS", lookAheadSS);
  		bundle.putInt("lookAheadPst", lookAheadPst);
  		bundle.putFloat("lookAheadB", lookAheadB);
  		bundle.putFloat("smoothB", smoothB);
  		bundle.putFloat("smoothBS", smoothBS);
  
  		bundle.putDoubleArray("mapCenter", currentViewport.mapCenter);
  		bundle.putDoubleArray("currentLocation", currentViewport.location);
  		bundle.putIntArray("mapCenterXY", currentViewport.mapCenterXY);
  		bundle.putIntArray("currentLocationXY", currentViewport.locationXY);
  		bundle.putFloat("mapHeading", currentViewport.mapHeading);
  		bundle.putFloat("bearing", currentViewport.bearing);
  		bundle.putFloat("speed", currentViewport.speed);
  		bundle.putDouble("mpp", mpp);
  		bundle.putInt("vectorLength", vectorLength);
  		bundle.putInt("proximity", proximity);
  
  		return bundle;
  	}
  
  	@Override
  	public Object getDraggableObjectAtPoint(PointInfo touchPoint)
  	{
  		pinch = 0;
  		scale = 1;
  		return this;
  	}
  
  	@Override
  	public void getPositionAndScale(Object obj, PositionAndScale objPosAndScaleOut)
  	{
  	}
  
  	@Override
  	public void selectObject(Object obj, PointInfo touchPoint)
  	{
  		if (obj == null)
  		{
  			pinch = 0;
  			try
  			{
  				mapHolder.zoomMap(scale);
  			}
  			catch (Exception e)
  			{
  				//ignore
  			}
  		}
  	}
  
  	@Override
  	public boolean setPositionAndScale(Object obj, PositionAndScale newObjPosAndScale, PointInfo touchPoint)
  	{
  		if (touchPoint.isDown() && touchPoint.getNumTouchPoints() == 2)
  		{
  			if (pinch == 0)
  			{
  				pinch = touchPoint.getMultiTouchDiameterSq();
  			}
  			scale = touchPoint.getMultiTouchDiameterSq() / pinch;
  			if (scale > 1)
  			{
  				scale = (float) (Math.log10(scale) + 1);
  			}
  			else
  			{
  				scale = (float) (1 / (Math.log10(1 / scale) + 1));
  			}
  		}
  		return true;
  	}
  }
  