package org.osmdroid.views.overlay.mylocation;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.location.Location;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import org.osmdroid.api.IMapController;
import org.osmdroid.api.IMapView;
import org.osmdroid.config.Configuration;
import org.osmdroid.library.R;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.util.TileSystem;
import org.osmdroid.views.MapView;
import org.osmdroid.views.Projection;
import org.osmdroid.views.overlay.IOverlayMenuProvider;
import org.osmdroid.views.overlay.Overlay;
import org.osmdroid.views.overlay.Overlay.Snappable;
import java.util.LinkedList;
/**
*
* @author Marc Kurtz
* @author Manuel Stahl
*
*/
public class MyLocationNewOverlay extends Overlay implements IMyLocationConsumer,
IOverlayMenuProvider, Snappable {
// ===========================================================
// Constants
// ===========================================================
// ===========================================================
// Fields
// ===========================================================
protected Paint mPaint = new Paint();
protected Paint mCirclePaint = new Paint();
protected final float mScale;
protected Bitmap mPersonBitmap;
protected Bitmap mDirectionArrowBitmap;
protected MapView mMapView;
private IMapController mMapController;
public IMyLocationProvider mMyLocationProvider;
private final LinkedList<Runnable> mRunOnFirstFix = new LinkedList<Runnable>();
private final Point mMapCoordsProjected = new Point();
private final Point mMapCoordsTranslated = new Point();
private Handler mHandler;
private Object mHandlerToken = new Object();
/**
* if true, when the user pans the map, follow my location will automatically disable
* if false, when the user pans the map, the map will continue to follow current location
*/
protected boolean enableAutoStop=true;
private Location mLocation;
private final GeoPoint mGeoPoint = new GeoPoint(0, 0); // for reuse
private boolean mIsLocationEnabled = false;
protected boolean mIsFollowing = false; // follow location updates
protected boolean mDrawAccuracyEnabled = true;
/** Coordinates the feet of the person are located scaled for display density. */
protected final PointF mPersonHotspot;
protected float mDirectionArrowCenterX;
protected float mDirectionArrowCenterY;
public static final int MENU_MY_LOCATION = getSafeMenuId();
private boolean mOptionsMenuEnabled = true;
// to avoid allocations during onDraw
private final float[] mMatrixValues = new float[9];
private Matrix mMatrix = new Matrix();
private Rect mMyLocationRect = new Rect();
private Rect mMyLocationPreviousRect = new Rect();
// ===========================================================
// Constructors
// ===========================================================
public MyLocationNewOverlay(MapView mapView) {
this(new GpsMyLocationProvider(mapView.getContext()), mapView);
}
public MyLocationNewOverlay(IMyLocationProvider myLocationProvider, MapView mapView) {
super();
mScale = mapView.getContext().getResources().getDisplayMetrics().density;
mMapView = mapView;
mMapController = mapView.getController();
mCirclePaint.setARGB(0, 100, 100, 255);
mCirclePaint.setAntiAlias(true);
mPaint.setFilterBitmap(true);
setDirectionArrow(((BitmapDrawable)mapView.getContext().getResources().getDrawable(R.drawable.person)).getBitmap(),
((BitmapDrawable)mapView.getContext().getResources().getDrawable(R.drawable.direction_arrow)).getBitmap());
// Calculate position of person icon's feet, scaled to screen density
mPersonHotspot = new PointF(24.0f * mScale + 0.5f, 39.0f * mScale + 0.5f);
mHandler = new Handler(Looper.getMainLooper());
setMyLocationProvider(myLocationProvider);
}
/**
* fix for https://github.com/osmdroid/osmdroid/issues/249
* @param personBitmap
* @param directionArrowBitmap
*/
public void setDirectionArrow(final Bitmap personBitmap, final Bitmap directionArrowBitmap){
this.mPersonBitmap = personBitmap;
this.mDirectionArrowBitmap=directionArrowBitmap;
mDirectionArrowCenterX = mDirectionArrowBitmap.getWidth() / 2.0f - 0.5f;
mDirectionArrowCenterY = mDirectionArrowBitmap.getHeight() / 2.0f - 0.5f;
}
@Override
public void onDetach(MapView mapView) {
this.disableMyLocation();
/*if (mPersonBitmap != null) {
mPersonBitmap.recycle();
}
if (mDirectionArrowBitmap != null) {
mDirectionArrowBitmap.recycle();
}*/
this.mMapView = null;
this.mMapController = null;
mHandler = null;
mMatrix = null;
mCirclePaint = null;
//mPersonBitmap = null;
//mDirectionArrowBitmap = null;
mHandlerToken = null;
mLocation = null;
mMapController = null;
mMyLocationPreviousRect = null;
if (mMyLocationProvider!=null)
mMyLocationProvider.destroy();
mMyLocationProvider = null;
super.onDetach(mapView);
}
// ===========================================================
// Getter & Setter
// ===========================================================
/**
* If enabled, an accuracy circle will be drawn around your current position.
*
* @param drawAccuracyEnabled
* whether the accuracy circle will be enabled
*/
public void setDrawAccuracyEnabled(final boolean drawAccuracyEnabled) {
mDrawAccuracyEnabled = drawAccuracyEnabled;
}
/**
* If enabled, an accuracy circle will be drawn around your current position.
*
* @return true if enabled, false otherwise
*/
public boolean isDrawAccuracyEnabled() {
return mDrawAccuracyEnabled;
}
public IMyLocationProvider getMyLocationProvider() {
return mMyLocationProvider;
}
protected void setMyLocationProvider(IMyLocationProvider myLocationProvider) {
if (myLocationProvider == null)
throw new RuntimeException(
"You must pass an IMyLocationProvider to setMyLocationProvider()");
if (isMyLocationEnabled())
stopLocationProvider();
mMyLocationProvider = myLocationProvider;
}
public void setPersonHotspot(float x, float y) {
mPersonHotspot.set(x, y);
}
protected void drawMyLocation(final Canvas canvas, final MapView mapView, final Location lastFix) {
final Projection pj = mapView.getProjection();
pj.toPixelsFromProjected(mMapCoordsProjected, mMapCoordsTranslated);
if (mDrawAccuracyEnabled) {
final float radius = lastFix.getAccuracy()
/ (float) TileSystem.GroundResolution(lastFix.getLatitude(),
mapView.getZoomLevel());
mCirclePaint.setAlpha(50);
mCirclePaint.setStyle(Style.FILL);
canvas.drawCircle(mMapCoordsTranslated.x, mMapCoordsTranslated.y, radius, mCirclePaint);
mCirclePaint.setAlpha(150);
mCirclePaint.setStyle(Style.STROKE);
canvas.drawCircle(mMapCoordsTranslated.x, mMapCoordsTranslated.y, radius, mCirclePaint);
}
canvas.getMatrix(mMatrix);
mMatrix.getValues(mMatrixValues);
if (Configuration.getInstance().isDebugMode()) {
final float tx = (-mMatrixValues[Matrix.MTRANS_X] + 20)
/ mMatrixValues[Matrix.MSCALE_X];
final float ty = (-mMatrixValues[Matrix.MTRANS_Y] + 90)
/ mMatrixValues[Matrix.MSCALE_Y];
canvas.drawText("Lat: " + lastFix.getLatitude(), tx, ty + 5, mPaint);
canvas.drawText("Lon: " + lastFix.getLongitude(), tx, ty + 20, mPaint);
canvas.drawText("Alt: " + lastFix.getAltitude(), tx, ty + 35, mPaint);
canvas.drawText("Acc: " + lastFix.getAccuracy(), tx, ty + 50, mPaint);
}
// Calculate real scale including accounting for rotation
float scaleX = (float) Math.sqrt(mMatrixValues[Matrix.MSCALE_X]
* mMatrixValues[Matrix.MSCALE_X] + mMatrixValues[Matrix.MSKEW_Y]
* mMatrixValues[Matrix.MSKEW_Y]);
float scaleY = (float) Math.sqrt(mMatrixValues[Matrix.MSCALE_Y]
* mMatrixValues[Matrix.MSCALE_Y] + mMatrixValues[Matrix.MSKEW_X]
* mMatrixValues[Matrix.MSKEW_X]);
if (lastFix.hasBearing()) {
canvas.save();
// Rotate the icon if we have a GPS fix, take into account if the map is already rotated
float mapRotation=mapView.getMapOrientation();
mapRotation=lastFix.getBearing();
if (mapRotation >=360.0f)
mapRotation=mapRotation-360f;
canvas.rotate(mapRotation, mMapCoordsTranslated.x, mMapCoordsTranslated.y);
// Counteract any scaling that may be happening so the icon stays the same size
canvas.scale(1 / scaleX, 1 / scaleY, mMapCoordsTranslated.x, mMapCoordsTranslated.y);
// Draw the bitmap
canvas.drawBitmap(mDirectionArrowBitmap, mMapCoordsTranslated.x
- mDirectionArrowCenterX, mMapCoordsTranslated.y - mDirectionArrowCenterY,
mPaint);
canvas.restore();
} else {
canvas.save();
// Unrotate the icon if the maps are rotated so the little man stays upright
canvas.rotate(-mMapView.getMapOrientation(), mMapCoordsTranslated.x,
mMapCoordsTranslated.y);
// Counteract any scaling that may be happening so the icon stays the same size
canvas.scale(1 / scaleX, 1 / scaleY, mMapCoordsTranslated.x, mMapCoordsTranslated.y);
// Draw the bitmap
canvas.drawBitmap(mPersonBitmap, mMapCoordsTranslated.x - mPersonHotspot.x,
mMapCoordsTranslated.y - mPersonHotspot.y, mPaint);
canvas.restore();
}
}
protected Rect getMyLocationDrawingBounds(int zoomLevel, Location lastFix, Rect reuse) {
if (reuse == null)
reuse = new Rect();
final Projection pj = mMapView.getProjection();
pj.toPixelsFromProjected(mMapCoordsProjected, mMapCoordsTranslated);
// Start with the bitmap bounds
if (lastFix.hasBearing()) {
// Get a square bounding box around the object, and expand by the length of the diagonal
// so as to allow for extra space for rotating
int widestEdge = (int) Math.ceil(Math.max(mDirectionArrowBitmap.getWidth(),
mDirectionArrowBitmap.getHeight()) * Math.sqrt(2));
reuse.set(mMapCoordsTranslated.x, mMapCoordsTranslated.y, mMapCoordsTranslated.x
+ widestEdge, mMapCoordsTranslated.y + widestEdge);
reuse.offset(-widestEdge / 2, -widestEdge / 2);
} else {
reuse.set(mMapCoordsTranslated.x, mMapCoordsTranslated.y, mMapCoordsTranslated.x
+ mPersonBitmap.getWidth(), mMapCoordsTranslated.y + mPersonBitmap.getHeight());
reuse.offset((int) (-mPersonHotspot.x + 0.5f), (int) (-mPersonHotspot.y + 0.5f));
}
// Add in the accuracy circle if enabled
if (mDrawAccuracyEnabled) {
final int radius = (int) Math.ceil(lastFix.getAccuracy()
/ (float) TileSystem.GroundResolution(lastFix.getLatitude(), zoomLevel));
reuse.union(mMapCoordsTranslated.x - radius, mMapCoordsTranslated.y - radius,
mMapCoordsTranslated.x + radius, mMapCoordsTranslated.y + radius);
final int strokeWidth = (int) Math.ceil(mCirclePaint.getStrokeWidth() == 0 ? 1
: mCirclePaint.getStrokeWidth());
reuse.inset(-strokeWidth, -strokeWidth);
}
return reuse;
}
// ===========================================================
// Methods from SuperClass/Interfaces
// ===========================================================
@Override
public void draw(Canvas c, MapView mapView, boolean shadow) {
if (shadow)
return;
if (mLocation != null && isMyLocationEnabled()) {
drawMyLocation(c, mapView, mLocation);
}
}
@Override
public boolean onSnapToItem(final int x, final int y, final Point snapPoint,
final IMapView mapView) {
if (this.mLocation != null) {
Projection pj = mMapView.getProjection();
pj.toPixelsFromProjected(mMapCoordsProjected, mMapCoordsTranslated);
snapPoint.x = mMapCoordsTranslated.x;
snapPoint.y = mMapCoordsTranslated.y;
final double xDiff = x - mMapCoordsTranslated.x;
final double yDiff = y - mMapCoordsTranslated.y;
boolean snap = xDiff * xDiff + yDiff * yDiff < 64;
if (Configuration.getInstance().isDebugMode()) {
Log.d(IMapView.LOGTAG, "snap=" + snap);
}
return snap;
} else {
return false;
}
}
public void setEnableAutoStop(boolean value){
this.enableAutoStop=value;
}
public boolean getEnableAutoStop(){
return this.enableAutoStop;
}
@Override
public boolean onTouchEvent(final MotionEvent event, final MapView mapView) {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
if (enableAutoStop)
this.disableFollowLocation();
else
return true;//prevent the pan
}
return super.onTouchEvent(event, mapView);
}
// ===========================================================
// Menu handling methods
// ===========================================================
@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) {
pMenu.add(0, MENU_MY_LOCATION + pMenuIdOffset, Menu.NONE,
pMapView.getContext().getResources().getString(R.string.my_location)
)
.setIcon(
pMapView.getContext().getResources().getDrawable(R.drawable.ic_menu_mylocation)
)
.setCheckable(true);
return true;
}
@Override
public boolean onPrepareOptionsMenu(final Menu pMenu, final int pMenuIdOffset,
final MapView pMapView) {
pMenu.findItem(MENU_MY_LOCATION + pMenuIdOffset).setChecked(this.isMyLocationEnabled());
return false;
}
@Override
public boolean onOptionsItemSelected(final MenuItem pItem, final int pMenuIdOffset,
final MapView pMapView) {
final int menuId = pItem.getItemId() - pMenuIdOffset;
if (menuId == MENU_MY_LOCATION) {
if (this.isMyLocationEnabled()) {
this.disableFollowLocation();
this.disableMyLocation();
} else {
this.enableFollowLocation();
this.enableMyLocation();
}
return true;
} else {
return false;
}
}
// ===========================================================
// Methods
// ===========================================================
/**
* Return a GeoPoint of the last known location, or null if not known.
*/
public GeoPoint getMyLocation() {
if (mLocation == null) {
return null;
} else {
return new GeoPoint(mLocation);
}
}
public Location getLastFix() {
return mLocation;
}
/**
* Enables "follow" functionality. The map will center on your current location and
* automatically scroll as you move. Scrolling the map in the UI will disable.
*/
public void enableFollowLocation() {
mIsFollowing = true;
// set initial location when enabled
if (isMyLocationEnabled()) {
Location location = mMyLocationProvider.getLastKnownLocation();
if (location != null) {
setLocation(location);
}
}
// Update the screen to see changes take effect
if (mMapView != null) {
mMapView.postInvalidate();
}
}
/**
* Disables "follow" functionality.
*/
public void disableFollowLocation() {
mIsFollowing = false;
}
/**
* If enabled, the map will center on your current location and automatically scroll as you
* move. Scrolling the map in the UI will disable.
*
* @return true if enabled, false otherwise
*/
public boolean isFollowLocationEnabled() {
return mIsFollowing;
}
@Override
public void onLocationChanged(final Location location, IMyLocationProvider source) {
if (location != null && mHandler!=null) {
// These location updates can come in from different threads
mHandler.postAtTime(new Runnable() {
@Override
public void run() {
setLocation(location);
for (final Runnable runnable : mRunOnFirstFix) {
new Thread(runnable).start();
}
mRunOnFirstFix.clear();
}
}, mHandlerToken, 0);
}
}
protected void setLocation(Location location) {
// If we had a previous location, let's get those bounds
Location oldLocation = mLocation;
if (oldLocation != null) {
this.getMyLocationDrawingBounds(mMapView.getZoomLevel(), oldLocation,
mMyLocationPreviousRect);
}
mLocation = location;
// Cache location point
mMapView.getProjection().toProjectedPixels(mLocation.getLatitude(),
mLocation.getLongitude(), mMapCoordsProjected);
if (mIsFollowing) {
mGeoPoint.setLatitude(mLocation.getLatitude());
mGeoPoint.setLongitude(mLocation.getLongitude());
mMapController.animateTo(mGeoPoint);
} else {
// Get new drawing bounds
this.getMyLocationDrawingBounds(mMapView.getZoomLevel(), mLocation, mMyLocationRect);
// If we had a previous location, merge in those bounds too
if (oldLocation != null) {
mMyLocationRect.union(mMyLocationPreviousRect);
}
final int left = mMyLocationRect.left;
final int top = mMyLocationRect.top;
final int right = mMyLocationRect.right;
final int bottom = mMyLocationRect.bottom;
// Invalidate the bounds
mMapView.invalidateMapCoordinates(left, top, right, bottom);
}
}
public boolean enableMyLocation(IMyLocationProvider myLocationProvider) {
// Set the location provider. This will call stopLocationProvider().
setMyLocationProvider(myLocationProvider);
boolean success = mMyLocationProvider.startLocationProvider(this);
mIsLocationEnabled = success;
// set initial location when enabled
if (success) {
Location location = mMyLocationProvider.getLastKnownLocation();
if (location != null) {
setLocation(location);
}
}
// Update the screen to see changes take effect
if (mMapView != null) {
mMapView.postInvalidate();
}
return success;
}
/**
* Enable receiving location updates from the provided IMyLocationProvider and show your
* location on the maps. You will likely want to call enableMyLocation() from your Activity's
* Activity.onResume() method, to enable the features of this overlay. Remember to call the
* corresponding disableMyLocation() in your Activity's Activity.onPause() method to turn off
* updates when in the background.
*/
public boolean enableMyLocation() {
return enableMyLocation(mMyLocationProvider);
}
/**
* Disable location updates
*/
public void disableMyLocation() {
mIsLocationEnabled = false;
stopLocationProvider();
// Update the screen to see changes take effect
if (mMapView != null) {
mMapView.postInvalidate();
}
}
protected void stopLocationProvider() {
if (mMyLocationProvider != null) {
mMyLocationProvider.stopLocationProvider();
}
if (mHandler!=null && mHandlerToken!=null)
mHandler.removeCallbacksAndMessages(mHandlerToken);
}
/**
* If enabled, the map is receiving location updates and drawing your location on the map.
*
* @return true if enabled, false otherwise
*/
public boolean isMyLocationEnabled() {
return mIsLocationEnabled;
}
/**
* Queues a runnable to be executed as soon as we have a location fix. If we already have a fix,
* we'll execute the runnable immediately and return true. If not, we'll hang on to the runnable
* and return false; as soon as we get a location fix, we'll run it in in a new thread.
*/
public boolean runOnFirstFix(final Runnable runnable) {
if (mMyLocationProvider != null && mLocation != null) {
new Thread(runnable).start();
return true;
} else {
mRunOnFirstFix.addLast(runnable);
return false;
}
}
/**
* enabls you to change the my location 'person' icon at runtime. note that the
* hotspot is not updated with this method. see
* {@link #setPersonHotspot}
* @param icon
*/
public void setPersonIcon(Bitmap icon){
mPersonBitmap = icon;
}
}