package nl.sogeti.android.gpstracker.actions.utils; import java.text.DateFormat; import java.util.Date; import nl.sogeti.android.gpstracker.db.GPStracking.Segments; import nl.sogeti.android.gpstracker.db.GPStracking.Waypoints; import nl.sogeti.android.gpstracker.util.UnitsI18n; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.CornerPathEffect; import android.graphics.DashPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Bitmap.Config; import android.location.Location; import android.net.Uri; import android.util.AttributeSet; import android.util.Log; import android.view.View; public class GraphCanvas extends View { private static final String TAG = "GraphCanvas"; public static final int TIMESPEEDGRAPH = 0; public static final int DISTANCESPEEDGRAPH = 1; public static final int TIMEALTITUDEGRAPH = 2; public static final int DISTANCEALTITUDEGRAPH = 3; private Uri mUri; private Bitmap mRenderBuffer; private Canvas mRenderCanvas; private Context mContext; private UnitsI18n mUnits; private int mGraphType = TIMESPEEDGRAPH; private long mEndTime; private long mStartTime; private double mDistance; private int mHeight; private int mWidth; private int mMinAxis; private int mMaxAxis; private double mMinAlititude; private double mMaxAlititude; private double mMaxSpeed; private double mDistanceDrawn; private long mStartTimeDrawn; private long mEndTimeDrawn; private boolean calculating; public GraphCanvas(Context context) { super(context); mContext = context; } public GraphCanvas( Context context, AttributeSet attrs ) { super(context, attrs); mContext = context; } public GraphCanvas( Context context, AttributeSet attrs, int defStyle ) { super(context, attrs, defStyle); mContext = context; } /** * Set the dataset for which to draw data. Also provide hints and helpers. * * @param uri * @param startTime * @param endTime * @param distance * @param minAlititude * @param maxAlititude * @param maxSpeed * @param units */ public void setData( Uri uri, long startTime, long endTime, double distance, double minAlititude, double maxAlititude, double maxSpeed, UnitsI18n units ) { boolean rerender = false; if( uri.equals( mUri ) ) { double distanceDrawnPercentage = mDistanceDrawn / mDistance; double duractionDrawnPercentage = (double)((1d+mEndTimeDrawn-mStartTimeDrawn) / (1d+mEndTime-mStartTime)); rerender = distanceDrawnPercentage < 0.99d || duractionDrawnPercentage < 0.99d; } else { rerender = true; } mUri = uri; mUnits = units; mMinAlititude = minAlititude; mMaxAlititude = maxAlititude; mMaxSpeed = maxSpeed; mStartTime = startTime; mEndTime = endTime; mDistance = distance; if( rerender && !calculating ) { renderGraph(); } } public void setType( int graphType) { if( mGraphType != graphType ) { mGraphType = graphType; renderGraph(); } } public int getType() { return mGraphType; } @Override protected void onSizeChanged( int w, int h, int oldw, int oldh ) { super.onSizeChanged( w, h, oldw, oldh ); if( mRenderBuffer == null || mRenderBuffer.getWidth() != w || mRenderBuffer.getHeight() != h ) { mRenderBuffer = Bitmap.createBitmap( w, h, Config.ARGB_8888 ); mRenderCanvas = new Canvas( mRenderBuffer ); renderGraph(); } } @Override protected void onDraw( Canvas canvas ) { super.onDraw(canvas); canvas.drawBitmap( mRenderBuffer, 0, 0, null ); } private void renderGraph() { // Log.d( TAG, "renderGraph() type "+mGraphType+" on "+mRenderBuffer ); calculating = true; if( mRenderBuffer != null ) { mRenderBuffer.eraseColor( Color.TRANSPARENT ); switch( mGraphType ) { case( TIMESPEEDGRAPH ): setupSpeedAxis(); drawTimeAxisGraphOnCanvas( new String[] { Waypoints.TIME, Waypoints.SPEED } ); drawSpeedsTexts(); drawTimeTexts(); break; case( DISTANCESPEEDGRAPH ): setupSpeedAxis(); drawDistanceAxisGraphOnCanvas( new String[] { Waypoints.LONGITUDE, Waypoints.LATITUDE, Waypoints.SPEED } ); drawSpeedsTexts(); drawDistanceTexts(); break; case( TIMEALTITUDEGRAPH ): setupAltitudeAxis(); drawTimeAxisGraphOnCanvas( new String[] { Waypoints.TIME, Waypoints.ALTITUDE } ); drawAltitudesTexts(); drawTimeTexts(); break; case( DISTANCEALTITUDEGRAPH ): setupAltitudeAxis(); drawDistanceAxisGraphOnCanvas( new String[] { Waypoints.LONGITUDE, Waypoints.LATITUDE, Waypoints.ALTITUDE } ); drawAltitudesTexts(); drawDistanceTexts(); break; default: break; } mDistanceDrawn = mDistance; mStartTimeDrawn = mStartTime; mEndTimeDrawn = mEndTime; calculating = false; postInvalidate(); } } /** * TODO * @param params */ private void drawDistanceAxisGraphOnCanvas( String[] params ) { ContentResolver resolver = mContext.getApplicationContext().getContentResolver(); Uri segmentsUri = Uri.withAppendedPath( mUri, "/segments" ); Uri waypointsUri = null; Cursor segments = null; Cursor waypoints = null; mWidth = mRenderCanvas.getWidth()-5; mHeight = mRenderCanvas.getHeight()-10; double[][] values ; int[][] valueDepth; double distance = 1; try { segments = resolver.query( segmentsUri, new String[]{ Segments._ID }, null, null, null ); values = new double[segments.getCount()][mWidth]; valueDepth = new int[segments.getCount()][mWidth]; if( segments.moveToFirst() ) { do { int segment = segments.getPosition(); long segmentId = segments.getLong( 0 ); waypointsUri = Uri.withAppendedPath( segmentsUri, segmentId+"/waypoints" ); try { waypoints = resolver.query( waypointsUri, params, null, null, null ); if( waypoints.moveToFirst() ) { Location lastLocation = null; Location currentLocation = null; do { currentLocation = new Location( this.getClass().getName() ); currentLocation.setLongitude( waypoints.getDouble( 0 ) ); currentLocation.setLatitude( waypoints.getDouble( 1 ) ); if( lastLocation != null ) { distance += lastLocation.distanceTo( currentLocation ); } lastLocation = currentLocation; double value = waypoints.getDouble( 2 ); if( value > 1 && segment < values.length ) { int x = (int) ((distance)*(mWidth-1) / mDistance); if( x < valueDepth[segment].length ) { valueDepth[segment][x]++; values[segment][x] = values[segment][x]+((value-values[segment][x])/valueDepth[segment][x]); } } } while( waypoints.moveToNext() ); } } finally { if( waypoints != null ) { waypoints.close(); } } } while( segments.moveToNext() ); } } finally { if( segments != null ) { segments.close(); } } for( int segment=0;segment<values.length;segment++) { for( int x=0;x<values[segment].length;x++) { if( valueDepth[segment][x] > 0 ) { values[segment][x] = translateValue( values[segment][x] ); } } } drawGraph( values, valueDepth ); } private void drawTimeAxisGraphOnCanvas( String[] params ) { ContentResolver resolver = mContext.getApplicationContext().getContentResolver(); Uri segmentsUri = Uri.withAppendedPath( mUri, "/segments" ); Uri waypointsUri = null; Cursor segments = null; Cursor waypoints = null; mWidth = mRenderCanvas.getWidth()-5; mHeight = mRenderCanvas.getHeight()-10; long duration = 1+mEndTime - mStartTime; double[][] values ; int[][] valueDepth; try { segments = resolver.query( segmentsUri, new String[]{ Segments._ID }, null, null, null ); values = new double[segments.getCount()][mWidth]; valueDepth = new int[segments.getCount()][mWidth]; if( segments.moveToFirst() ) { do { int segment = segments.getPosition(); long segmentId = segments.getLong( 0 ); waypointsUri = Uri.withAppendedPath( segmentsUri, segmentId+"/waypoints" ); try { waypoints = resolver.query( waypointsUri, params, null, null, null ); if( waypoints.moveToFirst() ) { do { long time = waypoints.getLong( 0 ); double value = waypoints.getDouble( 1 ); if( value > 1 && segment < values.length ) { int x = (int) ((time-mStartTime)*(mWidth-1) / duration); if( x < valueDepth[segment].length ) { valueDepth[segment][x]++; values[segment][x] = values[segment][x]+((value-values[segment][x])/valueDepth[segment][x]); } } } while( waypoints.moveToNext() ); } } finally { if( waypoints != null ) { waypoints.close(); } } } while( segments.moveToNext() ); } } finally { if( segments != null ) { segments.close(); } } for( int p=0;p<values.length;p++) { for( int x=0;x<values[p].length;x++) { if( valueDepth[p][x] > 0 ) { values[p][x] = translateValue( values[p][x] ); } } } drawGraph( values, valueDepth ); } private void setupAltitudeAxis() { mMinAxis = 4 * (int)mUnits.conversionFromMeterToSmall(mMinAlititude / 4) ; mMaxAxis = 4 + 4 * (int)mUnits.conversionFromMeterToSmall(mMaxAlititude / 4) ; } private void setupSpeedAxis() { mMinAxis = 0; mMaxAxis = 4 + 4 * (int)mUnits.conversionFromMetersPerSecond( mMaxSpeed / 4); } private void drawAltitudesTexts() { Paint white = new Paint(); white.setColor( Color.WHITE ); white.setAntiAlias( true ); mRenderCanvas.drawText( String.format( "%d %s", mMinAxis, mUnits.getDistanceSmallUnit() ) , 8, mHeight, white ); mRenderCanvas.drawText( String.format( "%d %s", (mMaxAxis+mMinAxis)/2, mUnits.getDistanceSmallUnit() ) , 8, 5+mHeight/2, white ); mRenderCanvas.drawText( String.format( "%d %s", mMaxAxis, mUnits.getDistanceSmallUnit() ), 8, 15, white ); } private void drawSpeedsTexts() { Paint white = new Paint(); white.setColor( Color.WHITE ); white.setAntiAlias( true ); mRenderCanvas.drawText( String.format( "%d %s", mMinAxis, mUnits.getSpeedUnit() ) , 8, mHeight, white ); mRenderCanvas.drawText( String.format( "%d %s", (mMaxAxis+mMinAxis)/2, mUnits.getSpeedUnit() ) , 8, 3+mHeight/2, white ); mRenderCanvas.drawText( String.format( "%d %s", mMaxAxis, mUnits.getSpeedUnit() ) , 8, 7+white.getTextSize(), white ); } private void drawTimeTexts() { DateFormat timeInstance = DateFormat.getTimeInstance( DateFormat.SHORT ); String start = timeInstance.format( new Date( mStartTime ) ); String half = timeInstance.format( new Date( (mEndTime+mStartTime)/2 ) ); String end = timeInstance.format( new Date( mEndTime ) ); Paint white = new Paint(); white.setColor( Color.WHITE ); white.setAntiAlias( true ); white.setTextAlign( Paint.Align.CENTER ); Path yAxis; yAxis = new Path(); yAxis.moveTo( 5, 5+mHeight/2 ); yAxis.lineTo( 5, 5 ); mRenderCanvas.drawTextOnPath( String.format( start ), yAxis, 0, white.getTextSize(), white ); yAxis = new Path(); yAxis.moveTo( 5+mWidth/2 , 5+mHeight/2 ); yAxis.lineTo( 5+mWidth/2 , 5 ); mRenderCanvas.drawTextOnPath( String.format( half ), yAxis, 0, -3, white ); yAxis = new Path(); yAxis.moveTo( 5+mWidth-1 , 5+mHeight/2 ); yAxis.lineTo( 5+mWidth-1 , 5 ); mRenderCanvas.drawTextOnPath( String.format( end ), yAxis, 0, -3, white ); } private void drawDistanceTexts() { String start = String.format( "%.0f %s", mUnits.conversionFromMeter( 0 ), mUnits.getDistanceUnit() ) ; String half = String.format( "%.0f %s", mUnits.conversionFromMeter( mDistance/2), mUnits.getDistanceUnit() ) ; String end = String.format( "%.0f %s", mUnits.conversionFromMeter( mDistance), mUnits.getDistanceUnit() ) ; Paint white = new Paint(); white.setColor( Color.WHITE ); white.setAntiAlias( true ); white.setTextAlign( Paint.Align.CENTER ); Path yAxis; yAxis = new Path(); yAxis.moveTo( 5, 5+mHeight/2 ); yAxis.lineTo( 5, 5 ); mRenderCanvas.drawTextOnPath( String.format( start ), yAxis, 0, white.getTextSize(), white ); yAxis = new Path(); yAxis.moveTo( 5+mWidth/2 , 5+mHeight/2 ); yAxis.lineTo( 5+mWidth/2 , 5 ); mRenderCanvas.drawTextOnPath( String.format( half ), yAxis, 0, -3, white ); yAxis = new Path(); yAxis.moveTo( 5+mWidth-1 , 5+mHeight/2 ); yAxis.lineTo( 5+mWidth-1 , 5 ); mRenderCanvas.drawTextOnPath( String.format( end ), yAxis, 0, -3, white ); } private double translateValue( double val ) { switch( mGraphType ) { case( TIMESPEEDGRAPH ): case( DISTANCESPEEDGRAPH ): val = mUnits.conversionFromMetersPerSecond( val ); break; case( TIMEALTITUDEGRAPH ): case( DISTANCEALTITUDEGRAPH ): val = mUnits.conversionFromMeterToSmall( val ); break; default: break; } return val; } private void drawGraph( double[][] values, int[][] valueDepth ) { // Matrix Paint ltgrey = new Paint(); ltgrey.setColor( Color.LTGRAY ); ltgrey.setStrokeWidth( 1 ); // Horizontals ltgrey.setPathEffect( new DashPathEffect( new float[]{2,4}, 0 ) ); mRenderCanvas.drawLine( 5, 5 , 5+mWidth, 5 , ltgrey ); // top mRenderCanvas.drawLine( 5, 5+mHeight/4 , 5+mWidth, 5+mHeight/4 , ltgrey ); // 2nd mRenderCanvas.drawLine( 5, 5+mHeight/2 , 5+mWidth, 5+mHeight/2 , ltgrey ); // middle mRenderCanvas.drawLine( 5, 5+mHeight/4*3, 5+mWidth, 5+mHeight/4*3, ltgrey ); // 3rd // Verticals mRenderCanvas.drawLine( 5+mWidth/4 , 5, 5+mWidth/4 , 5+mHeight, ltgrey ); // 2nd mRenderCanvas.drawLine( 5+mWidth/2 , 5, 5+mWidth/2 , 5+mHeight, ltgrey ); // middle mRenderCanvas.drawLine( 5+mWidth/4*3, 5, 5+mWidth/4*3, 5+mHeight, ltgrey ); // 3rd mRenderCanvas.drawLine( 5+mWidth-1 , 5, 5+mWidth-1 , 5+mHeight, ltgrey ); // right // The line Paint routePaint = new Paint(); routePaint.setPathEffect( new CornerPathEffect( 8 ) ); routePaint.setStyle( Paint.Style.STROKE ); routePaint.setStrokeWidth( 4 ); routePaint.setAntiAlias( true ); routePaint.setColor(Color.GREEN); Path mPath; mPath = new Path(); for( int p=0;p<values.length;p++) { int start = 0; while( valueDepth[p][start] == 0 && start < values[p].length-1 ) { start++; } mPath.moveTo( (float)start+5, 5f+ (float) ( mHeight - ( ( values[p][start]-mMinAxis )*mHeight ) / ( mMaxAxis-mMinAxis ) ) ); for( int x=start;x<values[p].length;x++) { double y = mHeight - ( ( values[p][x]-mMinAxis )*mHeight ) / ( mMaxAxis-mMinAxis ) ; if( valueDepth[p][x] > 0 ) { mPath.lineTo( (float)x+5, (float) y+5 ); } } } mRenderCanvas.drawPath( mPath, routePaint ); // Axis's Paint dkgrey = new Paint(); dkgrey.setColor( Color.DKGRAY ); dkgrey.setStrokeWidth( 2 ); mRenderCanvas.drawLine( 5, 5 , 5 , 5+mHeight, dkgrey ); mRenderCanvas.drawLine( 5, 5+mHeight, 5+mWidth, 5+mHeight, dkgrey ); } }