diff --git a/OsmAnd/res/values/colors.xml b/OsmAnd/res/values/colors.xml index 4edbf0c584..ffb81c8b21 100644 --- a/OsmAnd/res/values/colors.xml +++ b/OsmAnd/res/values/colors.xml @@ -258,6 +258,8 @@ #33888888 + #EE5622 + #ff33b5e5 #b9b9b9 diff --git a/OsmAnd/res/values/ids.xml b/OsmAnd/res/values/ids.xml index ca2120f6bb..9f4c60d87d 100644 --- a/OsmAnd/res/values/ids.xml +++ b/OsmAnd/res/values/ids.xml @@ -8,5 +8,8 @@ + + + \ No newline at end of file diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index f449fb6a95..93dfd0b4b8 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -12,6 +12,8 @@ --> + Show compass ruler + Hide compass ruler Select icon Mode: %s User Mode, derived from: %s diff --git a/OsmAnd/src/net/osmand/plus/OsmandSettings.java b/OsmAnd/src/net/osmand/plus/OsmandSettings.java index c716b57310..c8b182be4b 100644 --- a/OsmAnd/src/net/osmand/plus/OsmandSettings.java +++ b/OsmAnd/src/net/osmand/plus/OsmandSettings.java @@ -803,6 +803,8 @@ public class OsmandSettings { public final CommonPreference RULER_MODE = new EnumIntPreference<>("ruler_mode", RulerMode.FIRST, RulerMode.values()).makeGlobal(); + public final OsmandPreference SHOW_COMPASS_CONTROL_RULER = new BooleanPreference("show_compass_ruler", true).makeGlobal(); + public final CommonPreference SHOW_LINES_TO_FIRST_MARKERS = new BooleanPreference("show_lines_to_first_markers", false).makeProfile(); public final CommonPreference SHOW_ARROWS_TO_FIRST_MARKERS = new BooleanPreference("show_arrows_to_first_markers", false).makeProfile(); diff --git a/OsmAnd/src/net/osmand/plus/views/MapInfoLayer.java b/OsmAnd/src/net/osmand/plus/views/MapInfoLayer.java index 9f23043f1f..cf2a5cc49e 100644 --- a/OsmAnd/src/net/osmand/plus/views/MapInfoLayer.java +++ b/OsmAnd/src/net/osmand/plus/views/MapInfoLayer.java @@ -18,6 +18,7 @@ import net.osmand.plus.activities.MapActivity; import net.osmand.plus.helpers.AndroidUiHelper; import net.osmand.plus.mapcontextmenu.other.TrackDetailsMenu.TrackChartPoints; import net.osmand.plus.views.mapwidgets.MapInfoWidgetsFactory; +import net.osmand.plus.views.mapwidgets.MapInfoWidgetsFactory.CompassRulerControlWidgetState; import net.osmand.plus.views.mapwidgets.MapInfoWidgetsFactory.TopCoordinatesView; import net.osmand.plus.views.mapwidgets.MapInfoWidgetsFactory.TopTextView; import net.osmand.plus.views.mapwidgets.MapInfoWidgetsFactory.TopToolbarController; @@ -200,7 +201,7 @@ public class MapInfoLayer extends OsmandMapLayer { TextInfoWidget battery = ric.createBatteryControl(map); registerSideWidget(battery, R.drawable.ic_action_battery, R.string.map_widget_battery, "battery", false, 42); TextInfoWidget ruler = mic.createRulerControl(map); - registerSideWidget(ruler, R.drawable.ic_action_ruler_circle, R.string.map_widget_ruler_control, "ruler", false, 43); + registerSideWidget(ruler, new CompassRulerControlWidgetState(app), "ruler", false, 43); } public void recreateControls() { diff --git a/OsmAnd/src/net/osmand/plus/views/RulerControlLayer.java b/OsmAnd/src/net/osmand/plus/views/RulerControlLayer.java index b310063b13..ce67494d98 100644 --- a/OsmAnd/src/net/osmand/plus/views/RulerControlLayer.java +++ b/OsmAnd/src/net/osmand/plus/views/RulerControlLayer.java @@ -3,17 +3,23 @@ package net.osmand.plus.views; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Path; import android.graphics.PathMeasure; import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; import android.os.Handler; import android.os.Message; +import android.support.v4.content.ContextCompat; import android.view.MotionEvent; import android.view.View; +import net.osmand.AndroidUtils; import net.osmand.Location; import net.osmand.data.LatLon; import net.osmand.data.QuadPoint; @@ -36,6 +42,7 @@ public class RulerControlLayer extends OsmandMapLayer { private static final long DELAY_BEFORE_DRAW = 500; private static final int TEXT_SIZE = 14; private static final int DISTANCE_TEXT_SIZE = 16; + private static final int COMPASS_CIRCLE_ID = 2; private final MapActivity mapActivity; private OsmandApplication app; @@ -74,11 +81,26 @@ public class RulerControlLayer extends OsmandMapLayer { private Bitmap centerIconDay; private Bitmap centerIconNight; private Paint bitmapPaint; + private Paint triangleHeadingPaint; + private Paint triangleNorthPaint; + private Paint redLinesPaint; + private Paint blueLinesPaint; + private RenderingLineAttributes lineAttrs; private RenderingLineAttributes lineFontAttrs; private RenderingLineAttributes circleAttrs; private RenderingLineAttributes circleAttrsAlt; + private Path compass = new Path(); + private Path arrow = new Path(); + private Path arrowArc = new Path(); + private Path redCompassLines = new Path(); + + private double[] degrees = new double[72]; + private int[] arcColors = {Color.parseColor("#00237BFF"), Color.parseColor("#237BFF"), Color.parseColor("#00237BFF")}; + + private float cachedHeading = 0; + private Handler handler; public RulerControlLayer(MapActivity mapActivity) { @@ -118,6 +140,14 @@ public class RulerControlLayer extends OsmandMapLayer { bitmapPaint.setDither(true); bitmapPaint.setFilterBitmap(true); + int colorNorthArrow = ContextCompat.getColor(app, R.color.compass_control_active); + int colorHeadingArrow = ContextCompat.getColor(app, R.color.active_buttons_and_links_light); + + triangleNorthPaint = initPaintWithStyle(Style.FILL, colorNorthArrow); + triangleHeadingPaint = initPaintWithStyle(Style.FILL, colorHeadingArrow); + redLinesPaint = initPaintWithStyle(Style.STROKE, colorNorthArrow); + blueLinesPaint = initPaintWithStyle(Style.STROKE, colorHeadingArrow); + lineAttrs = new RenderingLineAttributes("rulerLine"); float circleTextSize = TEXT_SIZE * mapActivity.getResources().getDisplayMetrics().density; @@ -141,6 +171,18 @@ public class RulerControlLayer extends OsmandMapLayer { view.refreshMap(); } }; + + for (int i = 0; i < 72; i++) { + degrees[i] = Math.toRadians(i * 5); + } + } + + private Paint initPaintWithStyle(Paint.Style style, int color) { + Paint paint = new Paint(); + paint.setStyle(style); + paint.setColor(color); + paint.setAntiAlias(true); + return paint; } @Override @@ -190,6 +232,7 @@ public class RulerControlLayer extends OsmandMapLayer { circleAttrsAlt.paint2.setStyle(Style.FILL); final QuadPoint center = tb.getCenterPixelPoint(); final RulerMode mode = app.getSettings().RULER_MODE.get(); + boolean showCompass = app.getSettings().SHOW_COMPASS_CONTROL_RULER.get(); final long currentTime = System.currentTimeMillis(); if (cacheMultiTouchEndTime != view.getMultiTouchEndTime()) { @@ -226,9 +269,17 @@ public class RulerControlLayer extends OsmandMapLayer { } if (mode == RulerMode.FIRST || mode == RulerMode.SECOND) { updateData(tb, center); + if (showCompass) { + updateHeading(); + resetDrawingPaths(); + } RenderingLineAttributes attrs = mode == RulerMode.FIRST ? circleAttrs : circleAttrsAlt; for (int i = 1; i <= cacheDistances.size(); i++) { - drawCircle(canvas, tb, i, center, attrs); + if (showCompass && i == COMPASS_CIRCLE_ID) { + drawCompassCircle(canvas, tb, center, attrs); + } else { + drawCircle(canvas, tb, i, center, attrs); + } } } } @@ -239,6 +290,20 @@ public class RulerControlLayer extends OsmandMapLayer { rightWidgetsPanel.getVisibility() == View.VISIBLE; } + private void updateHeading() { + Float heading = mapActivity.getMapViewTrackingUtilities().getHeading(); + if (heading != null && heading != cachedHeading) { + cachedHeading = heading; + } + } + + private void resetDrawingPaths() { + redCompassLines.reset(); + arrowArc.reset(); + compass.reset(); + arrow.reset(); + } + private void refreshMapDelayed() { handler.sendEmptyMessageDelayed(0, DRAW_TIME + 50); } @@ -293,7 +358,7 @@ public class RulerControlLayer extends OsmandMapLayer { } private void drawCenterIcon(Canvas canvas, RotatedTileBox tb, QuadPoint center, boolean nightMode, - RulerMode mode) { + RulerMode mode) { canvas.rotate(-tb.getRotate(), center.x, center.y); if (nightMode || mode == RulerMode.SECOND) { canvas.drawBitmap(centerIconNight, center.x - centerIconNight.getWidth() / 2, @@ -391,42 +456,200 @@ public class RulerControlLayer extends OsmandMapLayer { } private void drawCircle(Canvas canvas, RotatedTileBox tb, int circleNumber, QuadPoint center, - RenderingLineAttributes attrs) { + RenderingLineAttributes attrs) { if (!tb.isZoomAnimated()) { - Rect bounds = new Rect(); + float circleRadius = radius * circleNumber; String text = cacheDistances.get(circleNumber - 1); - attrs.paint2.getTextBounds(text, 0, text.length(), bounds); - - // coords of left or top text - float x1 = 0; - float y1 = 0; - // coords of right or bottom text - float x2 = 0; - float y2 = 0; - - if (textSide == TextSide.VERTICAL) { - x1 = center.x - bounds.width() / 2; - y1 = center.y - radius * circleNumber + bounds.height() / 2; - x2 = center.x - bounds.width() / 2; - y2 = center.y + radius * circleNumber + bounds.height() / 2; - } else if (textSide == TextSide.HORIZONTAL) { - x1 = center.x - radius * circleNumber - bounds.width() / 2; - y1 = center.y + bounds.height() / 2; - x2 = center.x + radius * circleNumber - bounds.width() / 2; - y2 = center.y + bounds.height() / 2; - } + float[] textCoords = calculateDistanceTextCoords(text, circleRadius, center, attrs); canvas.rotate(-tb.getRotate(), center.x, center.y); canvas.drawCircle(center.x, center.y, radius * circleNumber, attrs.shadowPaint); canvas.drawCircle(center.x, center.y, radius * circleNumber, attrs.paint); - canvas.drawText(text, x1, y1, attrs.paint3); - canvas.drawText(text, x1, y1, attrs.paint2); - canvas.drawText(text, x2, y2, attrs.paint3); - canvas.drawText(text, x2, y2, attrs.paint2); + drawTextCoords(canvas, text, textCoords, attrs); canvas.rotate(tb.getRotate(), center.x, center.y); } } + private void drawTextCoords(Canvas canvas, String text, float[] textCoords, RenderingLineAttributes attrs) { + canvas.drawText(text, textCoords[0], textCoords[1], attrs.paint3); + canvas.drawText(text, textCoords[0], textCoords[1], attrs.paint2); + canvas.drawText(text, textCoords[2], textCoords[3], attrs.paint3); + canvas.drawText(text, textCoords[2], textCoords[3], attrs.paint2); + } + + private float[] calculateDistanceTextCoords(String text, float drawingTextRadius, QuadPoint center, RenderingLineAttributes attrs) { + Rect bounds = new Rect(); + attrs.paint2.getTextBounds(text, 0, text.length(), bounds); + + // coords of left or top text + float x1 = 0; + float y1 = 0; + // coords of right or bottom text + float x2 = 0; + float y2 = 0; + + if (textSide == TextSide.VERTICAL) { + x1 = center.x - bounds.width() / 2; + y1 = center.y - drawingTextRadius + bounds.height() / 2; + x2 = center.x - bounds.width() / 2; + y2 = center.y + drawingTextRadius + bounds.height() / 2; + } else if (textSide == TextSide.HORIZONTAL) { + x1 = center.x - drawingTextRadius - bounds.width() / 2; + y1 = center.y + bounds.height() / 2; + x2 = center.x + drawingTextRadius - bounds.width() / 2; + y2 = center.y + bounds.height() / 2; + } + return new float[]{x1, y1, x2, y2}; + } + + private void drawCompassCircle(Canvas canvas, RotatedTileBox tileBox, QuadPoint center, + RenderingLineAttributes attrs) { + if (!tileBox.isZoomAnimated()) { + float radiusLength = radius * COMPASS_CIRCLE_ID; + float innerRadiusLength = radiusLength - attrs.paint.getStrokeWidth() / 2; + + updateArcShader(radiusLength, center); + updateCompassPaths(center, innerRadiusLength, radiusLength); + drawCardinalDirections(canvas, center, radiusLength, tileBox, attrs); + + redLinesPaint.setStrokeWidth(attrs.paint.getStrokeWidth()); + blueLinesPaint.setStrokeWidth(attrs.paint.getStrokeWidth()); + + canvas.drawPath(compass, attrs.shadowPaint); + canvas.drawPath(compass, attrs.paint); + canvas.drawPath(redCompassLines, redLinesPaint); + + canvas.rotate(cachedHeading, center.x, center.y); + canvas.drawPath(arrowArc, blueLinesPaint); + canvas.rotate(-cachedHeading, center.x, center.y); + + canvas.drawPath(arrow, attrs.shadowPaint); + canvas.drawPath(arrow, triangleNorthPaint); + + canvas.rotate(cachedHeading, center.x, center.y); + canvas.drawPath(arrow, attrs.shadowPaint); + canvas.drawPath(arrow, triangleHeadingPaint); + canvas.rotate(-cachedHeading, center.x, center.y); + + String text = cacheDistances.get(COMPASS_CIRCLE_ID - 1); + float[] textCoords = calculateDistanceTextCoords(text, radiusLength + AndroidUtils.dpToPx(app, 16), center, attrs); + canvas.rotate(-tileBox.getRotate(), center.x, center.y); + drawTextCoords(canvas, text, textCoords, attrs); + canvas.rotate(tileBox.getRotate(), center.x, center.y); + } + } + + private void updateCompassPaths(QuadPoint center, float innerRadiusLength, float radiusLength) { + compass.addCircle(center.x, center.y, radiusLength, Path.Direction.CCW); + + arrowArc.addArc(new RectF(center.x - radiusLength, center.y - radiusLength, center.x + radiusLength, center.y + radiusLength), -45, -90); + + for (int i = 0; i < degrees.length; i++) { + double degree = degrees[i]; + float x = (float) Math.cos(degree); + float y = -(float) Math.sin(degree); + + float lineStartX = center.x + x * innerRadiusLength; + float lineStartY = center.y + y * innerRadiusLength; + + float lineLength = getCompassLineHeight(i); + + float lineStopX = center.x + x * (innerRadiusLength - lineLength); + float lineStopY = center.y + y * (innerRadiusLength - lineLength); + + if (i == 18) { + float shortLineMargin = AndroidUtils.dpToPx(app, 5.66f); + float shortLineHeight = AndroidUtils.dpToPx(app, 2.94f); + float startY = center.y + y * (radiusLength - shortLineMargin); + float stopY = center.y + y * (radiusLength - shortLineMargin - shortLineHeight); + + compass.moveTo(center.x, startY); + compass.lineTo(center.x, stopY); + + float firstPointY = center.y + y * (radiusLength + AndroidUtils.dpToPx(app, 5)); + + float secondPointX = center.x - AndroidUtils.dpToPx(app, 4); + float secondPointY = center.y + y * (radiusLength - AndroidUtils.dpToPx(app, 2)); + + float thirdPointX = center.x + AndroidUtils.dpToPx(app, 4); + float thirdPointY = center.y + y * (radiusLength - AndroidUtils.dpToPx(app, 2)); + + arrow.moveTo(center.x, firstPointY); + arrow.lineTo(secondPointX, secondPointY); + arrow.lineTo(thirdPointX, thirdPointY); + arrow.lineTo(center.x, firstPointY); + arrow.close(); + } else { + compass.moveTo(lineStartX, lineStartY); + compass.lineTo(lineStopX, lineStopY); + } + if (i % 9 == 0 && i % 6 != 0) { + redCompassLines.moveTo(lineStartX, lineStartY); + redCompassLines.lineTo(lineStopX, lineStopY); + } + } + } + + private float getCompassLineHeight(int index) { + if (index % 6 == 0) { + return AndroidUtils.dpToPx(app, 8); + } else if (index % 9 == 0 || index % 2 != 0) { + return AndroidUtils.dpToPx(app, 3); + } else { + return AndroidUtils.dpToPx(app, 6); + } + } + + private void drawCardinalDirections(Canvas canvas, QuadPoint center, float radiusLength, RotatedTileBox tileBox, RenderingLineAttributes attrs) { + float textMargin = AndroidUtils.dpToPx(app, 14); + attrs.paint2.setTextAlign(Paint.Align.CENTER); + attrs.paint3.setTextAlign(Paint.Align.CENTER); + + for (int i = 0; i < degrees.length; i += 9) { + String cardinalDirection = getCardinalDirection(i); + if (cardinalDirection != null) { + float textWidth = AndroidUtils.getTextWidth(attrs.paint2.getTextSize(), cardinalDirection); + + canvas.save(); + canvas.translate(center.x, center.y); + canvas.rotate(i * 5 + 90); + canvas.translate(0, radiusLength - textMargin - textWidth / 2); + canvas.rotate(-i * 5 - tileBox.getRotate() - 90); + + canvas.drawText(cardinalDirection, 0, 0, attrs.paint3); + canvas.drawText(cardinalDirection, 0, 0, attrs.paint2); + canvas.restore(); + } + } + } + + private void updateArcShader(float radiusLength, QuadPoint center) { + float arcLength = (float) (2 * Math.PI * radiusLength * (90f / 360)); + LinearGradient shader = new LinearGradient((float) (center.x - arcLength * 0.25), center.y, (float) (center.x + arcLength * 0.25), center.y, arcColors, null, Shader.TileMode.CLAMP); + blueLinesPaint.setShader(shader); + } + + private String getCardinalDirection(int i) { + if (i == 0) { + return "E"; + } else if (i == 9) { + return "NE"; + } else if (i == 18) { + return "N"; + } else if (i == 27) { + return "NW"; + } else if (i == 36) { + return "W"; + } else if (i == 45) { + return "SW"; + } else if (i == 54) { + return "S"; + } else if (i == 63) { + return "SE"; + } + return null; + } + private enum TextSide { VERTICAL, HORIZONTAL @@ -441,4 +664,4 @@ public class RulerControlLayer extends OsmandMapLayer { public boolean drawInScreenPixels() { return false; } -} +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/views/mapwidgets/MapInfoWidgetsFactory.java b/OsmAnd/src/net/osmand/plus/views/mapwidgets/MapInfoWidgetsFactory.java index a5da7b556e..2d1ee555b4 100644 --- a/OsmAnd/src/net/osmand/plus/views/mapwidgets/MapInfoWidgetsFactory.java +++ b/OsmAnd/src/net/osmand/plus/views/mapwidgets/MapInfoWidgetsFactory.java @@ -56,6 +56,7 @@ import net.osmand.plus.routing.RoutingHelper; import net.osmand.plus.views.OsmandMapLayer.DrawSettings; import net.osmand.plus.views.OsmandMapTileView; import net.osmand.plus.views.RulerControlLayer; +import net.osmand.plus.views.mapwidgets.MapWidgetRegistry.WidgetState; import net.osmand.plus.views.mapwidgets.NextTurnInfoWidget.TurnDrawable; import net.osmand.router.TurnType; import net.osmand.util.Algorithms; @@ -138,6 +139,54 @@ public class MapInfoWidgetsFactory { return gpsInfoControl; } + public static class CompassRulerControlWidgetState extends WidgetState { + + public static final int COMPASS_CONTROL_WIDGET_STATE_SHOW = R.id.compass_ruler_control_widget_state_show; + public static final int COMPASS_CONTROL_WIDGET_STATE_HIDE = R.id.compass_ruler_control_widget_state_hide; + + private final OsmandSettings.OsmandPreference showCompass; + + public CompassRulerControlWidgetState(OsmandApplication ctx) { + super(ctx); + showCompass = ctx.getSettings().SHOW_COMPASS_CONTROL_RULER; + } + + @Override + public int getMenuTitleId() { + return R.string.map_widget_ruler_control; + } + + @Override + public int getMenuIconId() { + return R.drawable.ic_action_ruler_circle; + } + + @Override + public int getMenuItemId() { + return showCompass.get() ? COMPASS_CONTROL_WIDGET_STATE_SHOW : COMPASS_CONTROL_WIDGET_STATE_HIDE; + } + + @Override + public int[] getMenuTitleIds() { + return new int[]{R.string.show_compass_ruler, R.string.hide_compass_ruler}; + } + + @Override + public int[] getMenuIconIds() { + return new int[]{R.drawable.ic_action_compass_widget, R.drawable.ic_action_compass_widget_hide}; + } + + @Override + public int[] getMenuItemIds() { + return new int[]{COMPASS_CONTROL_WIDGET_STATE_SHOW, COMPASS_CONTROL_WIDGET_STATE_HIDE}; + } + + @Override + public void changeState(int stateId) { + showCompass.set(stateId == COMPASS_CONTROL_WIDGET_STATE_SHOW); + } + } + public TextInfoWidget createRulerControl(final MapActivity map) { final String title = "—"; final TextInfoWidget rulerControl = new TextInfoWidget(map) {