// Created by plusminus on 15:00:17 - 23.11.2008
package org.androad.ui.map.overlay.util;
import java.util.List;
import org.osmdroid.util.GeoPoint;
import org.osmdroid.views.MapView.Projection;
import org.androad.preferences.PreferenceConstants;
import org.androad.util.constants.Constants;
import org.androad.util.constants.GeoConstants;
import org.androad.util.constants.MathematicalConstants;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.CornerPathEffect;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.Bitmap.Config;
import android.graphics.Paint.Cap;
import android.util.FloatMath;
public class ArrowPathCreator implements MathematicalConstants, PreferenceConstants, GeoConstants {
// ===========================================================
// Constants
// ===========================================================
private static final int HUD_ARROW_MIDDLETOPEAK = 13;
private static final int HUD_ARROW_MIDDLETOBOTTOM = 13;
private static final int HUD_ARROW_MIDDLETOSIDE = 13;
private static final int HUD_ARROW_SIDETOMIDDLE_OPEN = 5;
private static final int TURNARROW_SEGMENT_LENGTH_PX = 30;
private static final int TURNARROW_SEGMENT_HALF_LENGTH_PX = TURNARROW_SEGMENT_LENGTH_PX / 2;
private static final Paint pathTurnSegmentPeakPaint;
private static final Paint pathTurnSegmentPeakOutlinePaint;
private static final Paint pathTurnSegmentPaint;
private static final Paint pathTurnSegmentOutlinePaint;
static{
pathTurnSegmentPaint = new Paint();
pathTurnSegmentPaint.setARGB(255, 255, 255, 255); // white
pathTurnSegmentPaint.setStrokeWidth(8);
pathTurnSegmentPaint.setStyle(Paint.Style.STROKE);
pathTurnSegmentPaint.setStrokeCap(Cap.ROUND);
pathTurnSegmentOutlinePaint = new Paint(pathTurnSegmentPaint);
pathTurnSegmentOutlinePaint.setARGB(255, 0, 0, 0); // black
pathTurnSegmentOutlinePaint.setStrokeWidth(pathTurnSegmentPaint.getStrokeWidth()+2);
pathTurnSegmentPeakPaint = new Paint(pathTurnSegmentPaint);
pathTurnSegmentPeakPaint.setStyle(Paint.Style.FILL_AND_STROKE);
pathTurnSegmentPeakPaint.setStrokeWidth(1);
pathTurnSegmentPeakOutlinePaint = new Paint(pathTurnSegmentOutlinePaint);
pathTurnSegmentPeakOutlinePaint.setStyle(Paint.Style.STROKE);
pathTurnSegmentPeakOutlinePaint.setStrokeWidth(pathTurnSegmentPeakPaint.getStrokeWidth()+2);
pathTurnSegmentPaint.setAntiAlias(true);
pathTurnSegmentOutlinePaint.setAntiAlias(true);
pathTurnSegmentPeakPaint.setAntiAlias(true);
pathTurnSegmentPeakOutlinePaint.setAntiAlias(true);
pathTurnSegmentPaint.setPathEffect(new CornerPathEffect(pathTurnSegmentPaint.getStrokeWidth() / 2));
pathTurnSegmentOutlinePaint.setPathEffect(new CornerPathEffect(pathTurnSegmentOutlinePaint.getStrokeWidth() / 2));
pathTurnSegmentPeakPaint.setPathEffect(new CornerPathEffect(pathTurnSegmentPeakPaint.getStrokeWidth() / 2));
pathTurnSegmentPeakOutlinePaint.setPathEffect(new CornerPathEffect(pathTurnSegmentPeakOutlinePaint.getStrokeWidth() / 2));
}
// ===========================================================
// Fields
// ===========================================================
// ===========================================================
// Constructors
// ===========================================================
// ===========================================================
// Getter & Setter
// ===========================================================
public static Bitmap drawToBitmap(final Path arrowPath, final Path arrowPeakPath){
final Bitmap b = Bitmap.createBitmap(48, 48, Config.ARGB_8888);
final Canvas c = new Canvas(b);
final RectF boundsPath = new RectF();
arrowPath.computeBounds(boundsPath, true);
final RectF boundsOverall = new RectF();
arrowPath.computeBounds(boundsOverall, true);
boundsOverall.bottom = Math.max(boundsOverall.bottom, boundsPath.bottom);
boundsOverall.top = Math.min(boundsOverall.top, boundsPath.top);
boundsOverall.left = Math.min(boundsOverall.left, boundsPath.left);
boundsOverall.right = Math.max(boundsOverall.right, boundsPath.right);
final float width = boundsOverall.right - boundsOverall.left;
final float height = boundsOverall.top - boundsOverall.bottom;
final float biggerSide = Math.max(width, height);
final Matrix m = new Matrix();
final int PADDING = 6;
m.postTranslate(-boundsOverall.left + PADDING, -boundsOverall.top + PADDING); // 2px padding
final float scaleFactor = (48 - (2*PADDING)) / biggerSide;
m.postScale(scaleFactor, scaleFactor);
arrowPath.transform(m);
arrowPeakPath.transform(m);
c.drawPath(arrowPath, pathTurnSegmentOutlinePaint);
c.drawPath(arrowPath, pathTurnSegmentPaint);
c.drawPath(arrowPeakPath, pathTurnSegmentPeakOutlinePaint);
c.drawPath(arrowPeakPath, pathTurnSegmentPeakPaint);
return b;
}
public static void createArrowOverIndex(final Projection pj, final int indexOfArrow, final List<GeoPoint> polyLine, final Path pathTurnSegment, final Path pathTurnSegmentPeak, final float aScaleFactor, final int zoomLevel, final float turnAngle) throws IndexOutOfBoundsException{
final Point screenCoords = new Point();
final Point screenCoordsBefore = new Point();
final int lengthDesiredPerSide = (int)(TURNARROW_SEGMENT_HALF_LENGTH_PX / aScaleFactor);
final int polyLineLengthLessOne = polyLine.size() - 1;
/* Compute the first index to be used for the arrow. */
int startIndex = Math.max(0, indexOfArrow);
final float baseRestLen = lengthDesiredPerSide * (Math.max(1, (zoomLevel - 8)/ 2.5f)); /* length of the arrow increases as we get closer.*/
float restLen = baseRestLen;
GeoPoint mpCur = polyLine.get(startIndex);
pj.toMapPixels(mpCur, screenCoords);
float lastLen = 0;
/* While restLen not < 0 include the next element. */
while(startIndex > 0 && restLen > 0){
startIndex--;
/* calculate length from 'startIndex' to 'startIndex + 1' */
screenCoordsBefore.x = screenCoords.x;
screenCoordsBefore.y = screenCoords.y;
mpCur = polyLine.get(startIndex);
pj.toMapPixels(mpCur, screenCoords);
lastLen = FloatMath.sqrt(calculateScreenCoordDistanceSquared(screenCoords, screenCoordsBefore));
restLen -= lastLen;
}
final int shortenFirstTo = (int)Math.max(1,lastLen+restLen);
/* Compute the last index to be used for the arrow. */
int endIndex = indexOfArrow;
restLen = baseRestLen / (float)Math.max(1, Math.pow(2, 14 - zoomLevel));
mpCur = polyLine.get(endIndex);
pj.toMapPixels(mpCur, screenCoords);
float lastAngle = Constants.NOT_SET;
lastLen = 0;
while(endIndex < polyLineLengthLessOne && restLen > 0){
endIndex++;
/* calculate length from 'startIndex' to 'startIndex + 1' */
screenCoordsBefore.x = screenCoords.x;
screenCoordsBefore.y = screenCoords.y;
mpCur = polyLine.get(endIndex);
pj.toMapPixels(mpCur, screenCoords);
final float angle = org.androad.nav.util.Util.calculateBearing(screenCoordsBefore, screenCoords) - 90.0f;
if(lastAngle != Constants.NOT_SET && Math.abs(angle - lastAngle) > 30.0f){
restLen = 0;
endIndex--;
break;
}else{
lastLen = FloatMath.sqrt(calculateScreenCoordDistanceSquared(screenCoords, screenCoordsBefore));
restLen -= lastLen;
}
lastAngle = angle;
}
final int shortenLastTo = (int)Math.max(1, lastLen+restLen);
/* Create the path... */
mpCur = polyLine.get(startIndex);
pj.toMapPixels(mpCur, screenCoordsBefore);
mpCur = polyLine.get(startIndex+1);
pj.toMapPixels(mpCur, screenCoords);
/* First segment needs to get shrinked. */
shortenToDistance(screenCoords, screenCoordsBefore, shortenFirstTo, false);
pathTurnSegment.moveTo(screenCoordsBefore.x, screenCoordsBefore.y);
pathTurnSegment.lineTo(screenCoords.x, screenCoords.y);
for(int i = startIndex + 1; i < endIndex; i++){
mpCur = polyLine.get(i);
pj.toMapPixels(mpCur, screenCoords);
/* Add the onTurn-Point as second. */
pathTurnSegment.lineTo(screenCoords.x, screenCoords.y);
}
screenCoordsBefore.x = screenCoords.x;
screenCoordsBefore.y = screenCoords.y;
mpCur = polyLine.get(endIndex);
pj.toMapPixels(mpCur, screenCoords);
/* Determine the Angle the peak of the turn-arrow will point to, BEFORE shrinking. */
final float endAngle = org.androad.nav.util.Util.calculateBearing(screenCoordsBefore, screenCoords) - 90.0f;
/* First segment needs to get shrinked. */
shortenToDistance(screenCoordsBefore, screenCoords, shortenLastTo, false);
/* Add the onTurn-Point as last. */
pathTurnSegment.lineTo(screenCoords.x, screenCoords.y);
pathTurnSegmentPeak.moveTo(screenCoords.x - HUD_ARROW_SIDETOMIDDLE_OPEN / aScaleFactor, screenCoords.y + HUD_ARROW_MIDDLETOBOTTOM / aScaleFactor); // Start of the line
pathTurnSegmentPeak.lineTo(screenCoords.x - HUD_ARROW_MIDDLETOSIDE / aScaleFactor, screenCoords.y + HUD_ARROW_MIDDLETOBOTTOM / aScaleFactor); // Start at the lower left edge
pathTurnSegmentPeak.lineTo(screenCoords.x, screenCoords.y - HUD_ARROW_MIDDLETOPEAK / aScaleFactor); // Peak of the triangle
pathTurnSegmentPeak.lineTo(screenCoords.x + HUD_ARROW_MIDDLETOSIDE / aScaleFactor, screenCoords.y + HUD_ARROW_MIDDLETOBOTTOM / aScaleFactor); // Lower right edge
pathTurnSegmentPeak.lineTo(screenCoords.x + HUD_ARROW_SIDETOMIDDLE_OPEN / aScaleFactor, screenCoords.y + HUD_ARROW_MIDDLETOBOTTOM / aScaleFactor); // Small inset
/* Finally rotate the peak around the center-point, that it points to the correct direction of the turn. */
final Matrix rotPeak = new Matrix();
rotPeak.setRotate(endAngle, screenCoords.x, screenCoords.y);
pathTurnSegmentPeak.transform(rotPeak);
final float endAngleRad = (endAngle - 90) * DEGTORAD;
rotPeak.setTranslate(FloatMath.cos(endAngleRad) * 6, FloatMath.sin(endAngleRad) * 6);
pathTurnSegmentPeak.transform(rotPeak);
}
// ===========================================================
// Methods from SuperClass/Interfaces
// ===========================================================
// ===========================================================
// Methods
// ===========================================================
/**
* Tries to match the second parameter to the pxDistance.
* NOTE: distance will only be shorted, so it matches to "<code><= pxDistance</code>"
* @param fixedCoords will not be changed
* @param variableCoords will be changed, to match the pxDistance
*/
protected static final void shortenToDistance(final Point fixedCoords, final Point variableCoords, final int pxDesiredDistance, final boolean force){
final float curMaxDistSquared = calculateScreenCoordDistanceSquared(fixedCoords, variableCoords);
final float stretchFactor = pxDesiredDistance / (1.25f * FloatMath.sqrt(curMaxDistSquared));
if(stretchFactor > 1.0f) {
return; // stretching the distance over the actual points makes no sense.
} else{
final int dx = fixedCoords.x - variableCoords.x;
final int dy = fixedCoords.y - variableCoords.y;
variableCoords.x = fixedCoords.x - (int)(dx * stretchFactor);
variableCoords.y = fixedCoords.y - (int)(dy * stretchFactor);
}
}
protected static final int calculateScreenCoordDistanceSquared(final Point screenCoordsA, final Point screenCoordsB){
final int dx = screenCoordsA.x - screenCoordsB.x;
final int dy = screenCoordsA.y - screenCoordsB.y;
return dx*dx + dy*dy;
}
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
}