// Created by plusminus on 22:01:11 - 29.09.2008 package org.osmdroid.views.overlay.compass; import org.osmdroid.library.R; import org.osmdroid.views.MapView; import org.osmdroid.views.Projection; import org.osmdroid.views.overlay.IOverlayMenuProvider; import org.osmdroid.views.overlay.Overlay; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; import android.view.Display; import android.view.Menu; import android.view.MenuItem; import android.view.Surface; import android.view.WindowManager; /** * Note: the compass overlay causes issues on API 8 devices. See https://github.com/osmdroid/osmdroid/issues/218 * * <br><br> * Note: this class can cause issues if you're also relying on {@link MapView#addOnFirstLayoutListener} * If you happen to be using both, see <a href="https://github.com/osmdroid/osmdroid/issues/324">Issue 324</a> * @author Marc Kurtz * @author Manuel Stahl * */ public class CompassOverlay extends Overlay implements IOverlayMenuProvider, IOrientationConsumer { private Paint sSmoothPaint = new Paint(Paint.FILTER_BITMAP_FLAG); protected MapView mMapView; private final Display mDisplay; public IOrientationProvider mOrientationProvider; protected Bitmap mCompassFrameBitmap; protected Bitmap mCompassRoseBitmap; private final Matrix mCompassMatrix = new Matrix(); private boolean mIsCompassEnabled; /** * The bearing, in degrees east of north, or NaN if none has been set. */ private float mAzimuth = Float.NaN; private float mCompassCenterX = 35.0f; private float mCompassCenterY = 35.0f; private final float mCompassRadius = 20.0f; protected final float mCompassFrameCenterX; protected final float mCompassFrameCenterY; protected final float mCompassRoseCenterX; protected final float mCompassRoseCenterY; public static final int MENU_COMPASS = getSafeMenuId(); private boolean mOptionsMenuEnabled = true; protected final float mScale; // =========================================================== // Constructors // =========================================================== public CompassOverlay(Context context, MapView mapView) { this(context, new InternalCompassOrientationProvider(context), mapView); } public CompassOverlay(Context context, IOrientationProvider orientationProvider, MapView mapView) { super(); mScale = context.getResources().getDisplayMetrics().density; mMapView = mapView; final WindowManager windowManager = (WindowManager) context .getSystemService(Context.WINDOW_SERVICE); mDisplay = windowManager.getDefaultDisplay(); createCompassFramePicture(); createCompassRosePicture(); mCompassFrameCenterX = mCompassFrameBitmap.getWidth() / 2 - 0.5f; mCompassFrameCenterY = mCompassFrameBitmap.getHeight() / 2 - 0.5f; mCompassRoseCenterX = mCompassRoseBitmap.getWidth() / 2 - 0.5f; mCompassRoseCenterY = mCompassRoseBitmap.getHeight() / 2 - 0.5f; setOrientationProvider(orientationProvider); } @Override public void onDetach(MapView mapView) { this.mMapView=null; sSmoothPaint=null; this.disableCompass(); mCompassFrameBitmap.recycle(); mCompassRoseBitmap.recycle(); super.onDetach(mapView); } private void invalidateCompass() { Rect screenRect = mMapView.getProjection().getScreenRect(); final int frameLeft = screenRect.left + (int) Math.ceil((mCompassCenterX - mCompassFrameCenterX) * mScale); final int frameTop = screenRect.top + (int) Math.ceil((mCompassCenterY - mCompassFrameCenterY) * mScale); final int frameRight = screenRect.left + (int) Math.ceil((mCompassCenterX + mCompassFrameCenterX) * mScale); final int frameBottom = screenRect.top + (int) Math.ceil((mCompassCenterY + mCompassFrameCenterY) * mScale); // Expand by 2 to cover stroke width mMapView.postInvalidateMapCoordinates(frameLeft - 2, frameTop - 2, frameRight + 2, frameBottom + 2); } // =========================================================== // Getter & Setter // =========================================================== public void setCompassCenter(final float x, final float y) { mCompassCenterX = x; mCompassCenterY = y; } public IOrientationProvider getOrientationProvider() { return mOrientationProvider; } public void setOrientationProvider(IOrientationProvider orientationProvider) throws RuntimeException { if (orientationProvider == null) throw new RuntimeException( "You must pass an IOrientationProvider to setOrientationProvider()"); if (isCompassEnabled()) mOrientationProvider.stopOrientationProvider(); mOrientationProvider = orientationProvider; } protected void drawCompass(final Canvas canvas, final float bearing, final Rect screenRect) { final Projection proj = mMapView.getProjection(); final float centerX = mCompassCenterX * mScale; final float centerY = mCompassCenterY * mScale; mCompassMatrix.setTranslate(-mCompassFrameCenterX, -mCompassFrameCenterY); mCompassMatrix.postTranslate(centerX, centerY); canvas.save(); canvas.concat(proj.getInvertedScaleRotateCanvasMatrix()); canvas.concat(mCompassMatrix); canvas.drawBitmap(mCompassFrameBitmap, 0, 0, sSmoothPaint); canvas.restore(); mCompassMatrix.setRotate(-bearing, mCompassRoseCenterX, mCompassRoseCenterY); mCompassMatrix.postTranslate(-mCompassRoseCenterX, -mCompassRoseCenterY); mCompassMatrix.postTranslate(centerX, centerY); canvas.save(); canvas.concat(proj.getInvertedScaleRotateCanvasMatrix()); canvas.concat(mCompassMatrix); canvas.drawBitmap(mCompassRoseBitmap, 0, 0, sSmoothPaint); canvas.restore(); } // =========================================================== // Methods from SuperClass/Interfaces // =========================================================== @Override public void draw(Canvas c, MapView mapView, boolean shadow) { if (shadow) { return; } if (isCompassEnabled() && !Float.isNaN(mAzimuth)) { drawCompass(c, mAzimuth + getDisplayOrientation(), mapView.getProjection() .getScreenRect()); } } // =========================================================== // 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_COMPASS + pMenuIdOffset, Menu.NONE, pMapView.getContext().getResources().getString(R.string.compass)) .setIcon(pMapView.getContext().getResources().getDrawable(R.drawable.ic_menu_compass)) .setCheckable(true); return true; } @Override public boolean onPrepareOptionsMenu(final Menu pMenu, final int pMenuIdOffset, final MapView pMapView) { pMenu.findItem(MENU_COMPASS + pMenuIdOffset).setChecked(this.isCompassEnabled()); return false; } @Override public boolean onOptionsItemSelected(final MenuItem pItem, final int pMenuIdOffset, final MapView pMapView) { final int menuId = pItem.getItemId() - pMenuIdOffset; if (menuId == MENU_COMPASS) { if (this.isCompassEnabled()) { this.disableCompass(); } else { this.enableCompass(); } return true; } else { return false; } } // =========================================================== // Methods // =========================================================== @Override public void onOrientationChanged(float orientation, IOrientationProvider source) { mAzimuth = orientation; this.invalidateCompass(); } public boolean enableCompass(IOrientationProvider orientationProvider) { // Set the orientation provider. This will call stopOrientationProvider(). setOrientationProvider(orientationProvider); boolean success = mOrientationProvider.startOrientationProvider(this); mIsCompassEnabled = success; // Update the screen to see changes take effect if (mMapView != null) { this.invalidateCompass(); } return success; } /** * Enable receiving orientation updates from the provided IOrientationProvider and show a * compass on the map. You will likely want to call enableCompass() from your Activity's * Activity.onResume() method, to enable the features of this overlay. Remember to call the * corresponding disableCompass() in your Activity's Activity.onPause() method to turn off * updates when in the background. */ public boolean enableCompass() { return enableCompass(mOrientationProvider); } /** * Disable orientation updates */ public void disableCompass() { mIsCompassEnabled = false; if (mOrientationProvider != null) { mOrientationProvider.stopOrientationProvider(); } mOrientationProvider=null; // Reset values mAzimuth = Float.NaN; // Update the screen to see changes take effect if (mMapView != null) { this.invalidateCompass(); } } /** * If enabled, the map is receiving orientation updates and drawing your location on the map. * * @return true if enabled, false otherwise */ public boolean isCompassEnabled() { return mIsCompassEnabled; } public float getOrientation() { return mAzimuth; } // =========================================================== // Inner and Anonymous Classes // =========================================================== private Point calculatePointOnCircle(final float centerX, final float centerY, final float radius, final float degrees) { // for trigonometry, 0 is pointing east, so subtract 90 // compass degrees are the wrong way round final double dblRadians = Math.toRadians(-degrees + 90); final int intX = (int) (radius * Math.cos(dblRadians)); final int intY = (int) (radius * Math.sin(dblRadians)); return new Point((int) centerX + intX, (int) centerY - intY); } private void drawTriangle(final Canvas canvas, final float x, final float y, final float radius, final float degrees, final Paint paint) { canvas.save(); final Point point = this.calculatePointOnCircle(x, y, radius, degrees); canvas.rotate(degrees, point.x, point.y); final Path p = new Path(); p.moveTo(point.x - 2 * mScale, point.y); p.lineTo(point.x + 2 * mScale, point.y); p.lineTo(point.x, point.y - 5 * mScale); p.close(); canvas.drawPath(p, paint); canvas.restore(); } private int getDisplayOrientation() { switch (mDisplay.getOrientation()) { case Surface.ROTATION_90: return 90; case Surface.ROTATION_180: return 180; case Surface.ROTATION_270: return 270; default: return 0; } } private void createCompassFramePicture() { // The inside of the compass is white and transparent final Paint innerPaint = new Paint(); innerPaint.setColor(Color.WHITE); innerPaint.setAntiAlias(true); innerPaint.setStyle(Style.FILL); innerPaint.setAlpha(200); // The outer part (circle and little triangles) is gray and transparent final Paint outerPaint = new Paint(); outerPaint.setColor(Color.GRAY); outerPaint.setAntiAlias(true); outerPaint.setStyle(Style.STROKE); outerPaint.setStrokeWidth(2.0f); outerPaint.setAlpha(200); final int picBorderWidthAndHeight = (int) ((mCompassRadius + 5) * 2 * mScale); final int center = picBorderWidthAndHeight / 2; if (mCompassFrameBitmap!=null) mCompassFrameBitmap.recycle(); mCompassFrameBitmap = Bitmap.createBitmap(picBorderWidthAndHeight, picBorderWidthAndHeight, Config.ARGB_8888); final Canvas canvas = new Canvas(mCompassFrameBitmap); // draw compass inner circle and border canvas.drawCircle(center, center, mCompassRadius * mScale, innerPaint); canvas.drawCircle(center, center, mCompassRadius * mScale, outerPaint); // Draw little triangles north, south, west and east (don't move) // to make those move use "-bearing + 0" etc. (Note: that would mean to draw the triangles // in the onDraw() method) drawTriangle(canvas, center, center, mCompassRadius * mScale, 0, outerPaint); drawTriangle(canvas, center, center, mCompassRadius * mScale, 90, outerPaint); drawTriangle(canvas, center, center, mCompassRadius * mScale, 180, outerPaint); drawTriangle(canvas, center, center, mCompassRadius * mScale, 270, outerPaint); } private void createCompassRosePicture() { // Paint design of north triangle (it's common to paint north in red color) final Paint northPaint = new Paint(); northPaint.setColor(0xFFA00000); northPaint.setAntiAlias(true); northPaint.setStyle(Style.FILL); northPaint.setAlpha(220); // Paint design of south triangle (black) final Paint southPaint = new Paint(); southPaint.setColor(Color.BLACK); southPaint.setAntiAlias(true); southPaint.setStyle(Style.FILL); southPaint.setAlpha(220); // Create a little white dot in the middle of the compass rose final Paint centerPaint = new Paint(); centerPaint.setColor(Color.WHITE); centerPaint.setAntiAlias(true); centerPaint.setStyle(Style.FILL); centerPaint.setAlpha(220); final int picBorderWidthAndHeight = (int) ((mCompassRadius + 5) * 2 * mScale); final int center = picBorderWidthAndHeight / 2; if (mCompassRoseBitmap!=null) mCompassRoseBitmap.recycle(); mCompassRoseBitmap = Bitmap.createBitmap(picBorderWidthAndHeight, picBorderWidthAndHeight, Config.ARGB_8888); final Canvas canvas = new Canvas(mCompassRoseBitmap); // Blue triangle pointing north final Path pathNorth = new Path(); pathNorth.moveTo(center, center - (mCompassRadius - 3) * mScale); pathNorth.lineTo(center + 4 * mScale, center); pathNorth.lineTo(center - 4 * mScale, center); pathNorth.lineTo(center, center - (mCompassRadius - 3) * mScale); pathNorth.close(); canvas.drawPath(pathNorth, northPaint); // Red triangle pointing south final Path pathSouth = new Path(); pathSouth.moveTo(center, center + (mCompassRadius - 3) * mScale); pathSouth.lineTo(center + 4 * mScale, center); pathSouth.lineTo(center - 4 * mScale, center); pathSouth.lineTo(center, center + (mCompassRadius - 3) * mScale); pathSouth.close(); canvas.drawPath(pathSouth, southPaint); // Draw a little white dot in the middle canvas.drawCircle(center, center, 2, centerPaint); } }