/*------------------------------------------------------------------------------
** Ident: Sogeti Smart Mobile Solutions
** Author: rene
** Copyright: (c) Apr 24, 2011 Sogeti Nederland B.V. All Rights Reserved.
**------------------------------------------------------------------------------
** Sogeti Nederland B.V. | No part of this file may be reproduced
** Distributed Software Engineering | or transmitted in any form or by any
** Lange Dreef 17 | means, electronic or mechanical, for the
** 4131 NJ Vianen | purpose, without the express written
** The Netherlands | permission of the copyright holder.
*------------------------------------------------------------------------------
*
* This file is part of OpenGPSTracker.
*
* OpenGPSTracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenGPSTracker is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenGPSTracker. If not, see <http://www.gnu.org/licenses/>.
*
*/
package nl.sogeti.android.gpstracker.actions.utils;
import java.text.DateFormat;
import java.util.Date;
import nl.sogeti.android.gpstracker.R;
import nl.sogeti.android.gpstracker.db.GPStracking.Segments;
import nl.sogeti.android.gpstracker.db.GPStracking.Waypoints;
import nl.sogeti.android.gpstracker.util.Constants;
import nl.sogeti.android.gpstracker.util.UnitsI18n;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
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.Typeface;
import android.location.Location;
import android.net.Uri;
import android.util.AttributeSet;
import android.view.View;
/**
* Calculate and draw graphs of track data
*
* @version $Id: GraphCanvas.java 1133 2011-10-09 20:00:57Z rcgroot $
* @author rene (c) Mar 22, 2009, Sogeti B.V.
*/
public class GraphCanvas extends View
{
@SuppressWarnings("unused")
private static final String TAG = "OGT.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 = -1;
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 mHighestSpeedNumber;
private double mDistanceDrawn;
private long mStartTimeDrawn;
private long mEndTimeDrawn;
float density = Resources.getSystem().getDisplayMetrics().density;
private Paint whiteText ;
private Paint ltgreyMatrixDashed;
private Paint greenGraphLine;
private Paint dkgreyMatrixLine;
private Paint whiteCenteredText;
private Paint dkgrayLargeType;
public GraphCanvas( Context context, AttributeSet attrs )
{
this(context, attrs, 0);
}
public GraphCanvas( Context context, AttributeSet attrs, int defStyle )
{
super(context, attrs, defStyle);
mContext = context;
whiteText = new Paint();
whiteText.setColor( Color.WHITE );
whiteText.setAntiAlias( true );
whiteText.setTextSize( (int)(density * 12) );
whiteCenteredText = new Paint();
whiteCenteredText.setColor( Color.WHITE );
whiteCenteredText.setAntiAlias( true );
whiteCenteredText.setTextAlign( Paint.Align.CENTER );
whiteCenteredText.setTextSize( (int)(density * 12) );
ltgreyMatrixDashed = new Paint();
ltgreyMatrixDashed.setColor( Color.LTGRAY );
ltgreyMatrixDashed.setStrokeWidth( 1 );
ltgreyMatrixDashed.setPathEffect( new DashPathEffect( new float[]{2,4}, 0 ) );
greenGraphLine = new Paint();
greenGraphLine.setPathEffect( new CornerPathEffect( 8 ) );
greenGraphLine.setStyle( Paint.Style.STROKE );
greenGraphLine.setStrokeWidth( 4 );
greenGraphLine.setAntiAlias( true );
greenGraphLine.setColor(Color.GREEN);
dkgreyMatrixLine = new Paint();
dkgreyMatrixLine.setColor( Color.DKGRAY );
dkgreyMatrixLine.setStrokeWidth( 2 );
dkgrayLargeType = new Paint();
dkgrayLargeType.setColor( Color.LTGRAY );
dkgrayLargeType.setAntiAlias( true );
dkgrayLargeType.setTextAlign( Paint.Align.CENTER );
dkgrayLargeType.setTextSize( (int)(density * 21) );
dkgrayLargeType.setTypeface( Typeface.DEFAULT_BOLD );
}
/**
* 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, StatisticsCalulator calc )
{
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
{
if( mRenderBuffer == null && super.getWidth() > 0 && super.getHeight() > 0 )
{
initRenderBuffer(super.getWidth(), super.getHeight());
}
rerender = true;
}
mUri = uri;
mUnits = calc.getUnits();
mMinAlititude = mUnits.conversionFromMeterToHeight( calc.getMinAltitude() );
mMaxAlititude = mUnits.conversionFromMeterToHeight( calc.getMaxAltitude() );
if( mUnits.isUnitFlipped() )
{
mHighestSpeedNumber = 1.5 * mUnits.conversionFromMetersPerSecond( calc.getAverageStatisicsSpeed() );
}
else
{
mHighestSpeedNumber = mUnits.conversionFromMetersPerSecond( calc.getMaxSpeed() );
}
mStartTime = calc.getStarttime();
mEndTime = calc.getEndtime();
mDistance = calc.getDistanceTraveled();
if( rerender )
{
renderGraph();
}
}
public synchronized void clearData()
{
mUri = null;
mUnits = null;
mRenderBuffer = null;
}
public void setType( int graphType)
{
if( mGraphType != graphType )
{
mGraphType = graphType;
renderGraph();
}
}
public int getType()
{
return mGraphType;
}
@Override
protected synchronized void onSizeChanged( int w, int h, int oldw, int oldh )
{
super.onSizeChanged( w, h, oldw, oldh );
initRenderBuffer(w, h);
renderGraph();
}
private void initRenderBuffer(int w, int h)
{
mRenderBuffer = Bitmap.createBitmap( w, h, Config.ARGB_8888 );
mRenderCanvas = new Canvas( mRenderBuffer );
}
@Override
protected synchronized void onDraw( Canvas canvas )
{
super.onDraw(canvas);
if( mRenderBuffer != null )
{
canvas.drawBitmap( mRenderBuffer, 0, 0, null );
}
}
private synchronized void renderGraph()
{
if( mRenderBuffer != null && mUri != null )
{
mRenderBuffer.eraseColor( Color.TRANSPARENT );
switch( mGraphType )
{
case( TIMESPEEDGRAPH ):
setupSpeedAxis();
drawGraphType();
drawTimeAxisGraphOnCanvas( new String[] { Waypoints.TIME, Waypoints.SPEED }, Constants.MIN_STATISTICS_SPEED );
drawSpeedsTexts();
drawTimeTexts();
break;
case( DISTANCESPEEDGRAPH ):
setupSpeedAxis();
drawGraphType();
drawDistanceAxisGraphOnCanvas( new String[] { Waypoints.LONGITUDE, Waypoints.LATITUDE, Waypoints.SPEED }, Constants.MIN_STATISTICS_SPEED );
drawSpeedsTexts();
drawDistanceTexts();
break;
case( TIMEALTITUDEGRAPH ):
setupAltitudeAxis();
drawGraphType();
drawTimeAxisGraphOnCanvas( new String[] { Waypoints.TIME, Waypoints.ALTITUDE }, -1000d );
drawAltitudesTexts();
drawTimeTexts();
break;
case( DISTANCEALTITUDEGRAPH ):
setupAltitudeAxis();
drawGraphType();
drawDistanceAxisGraphOnCanvas( new String[] { Waypoints.LONGITUDE, Waypoints.LATITUDE, Waypoints.ALTITUDE }, -1000d );
drawAltitudesTexts();
drawDistanceTexts();
break;
default:
break;
}
mDistanceDrawn = mDistance;
mStartTimeDrawn = mStartTime;
mEndTimeDrawn = mEndTime;
}
postInvalidate();
}
/**
*
* @param params
* @param minValue Minimum value of params[1] that will be drawn
*/
private void drawDistanceAxisGraphOnCanvas( String[] params, double minValue )
{
ContentResolver resolver = mContext.getContentResolver();
Uri segmentsUri = Uri.withAppendedPath( mUri, "segments" );
Uri waypointsUri = null;
Cursor segments = null;
Cursor waypoints = null;
double[][] values ;
int[][] valueDepth;
double distance = 1;
try
{
segments = resolver.query(
segmentsUri,
new String[]{ Segments._ID },
null, null, null );
int segmentCount = segments.getCount();
values = new double[segmentCount][mWidth];
valueDepth = new int[segmentCount][mWidth];
if( segments.moveToFirst() )
{
for(int segment=0;segment<segmentCount;segment++)
{
segments.moveToPosition( segment );
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 ) );
// Do no include obvious wrong 0.0 lat 0.0 long, skip to next value in while-loop
if( currentLocation.getLatitude() == 0.0d || currentLocation.getLongitude() == 0.0d )
{
continue;
}
if( lastLocation != null )
{
distance += lastLocation.distanceTo( currentLocation );
}
lastLocation = currentLocation;
double value = waypoints.getDouble( 2 );
if( value != 0 && value > minValue && segment < values.length )
{
int x = (int) ((distance)*(mWidth-1) / mDistance);
if( x > 0 && 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();
}
}
}
}
}
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, double minValue )
{
ContentResolver resolver = mContext.getContentResolver();
Uri segmentsUri = Uri.withAppendedPath( mUri, "segments" );
Uri waypointsUri = null;
Cursor segments = null;
Cursor waypoints = null;
long duration = 1+mEndTime - mStartTime;
double[][] values ;
int[][] valueDepth;
try
{
segments = resolver.query(
segmentsUri,
new String[]{ Segments._ID },
null, null, null );
int segmentCount = segments.getCount();
values = new double[segmentCount][mWidth];
valueDepth = new int[segmentCount][mWidth];
if( segments.moveToFirst() )
{
for(int segment=0;segment<segmentCount;segment++)
{
segments.moveToPosition( segment );
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 != 0 && value > minValue && segment < values.length )
{
int x = (int) ((time-mStartTime)*(mWidth-1) / duration);
if( x > 0 && 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();
}
}
}
}
}
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 + 4 * (int)(mMinAlititude / 4);
mMaxAxis = 4 + 4 * (int)(mMaxAlititude / 4);
mWidth = mRenderCanvas.getWidth()-5;
mHeight = mRenderCanvas.getHeight()-10;
}
private void setupSpeedAxis()
{
mMinAxis = 0;
mMaxAxis = 4 + 4 * (int)( mHighestSpeedNumber / 4);
mWidth = mRenderCanvas.getWidth()-5;
mHeight = mRenderCanvas.getHeight()-10;
}
private void drawAltitudesTexts()
{
mRenderCanvas.drawText( String.format( "%d %s", mMinAxis, mUnits.getHeightUnit() ) , 8, mHeight, whiteText );
mRenderCanvas.drawText( String.format( "%d %s", (mMaxAxis+mMinAxis)/2, mUnits.getHeightUnit() ) , 8, 5+mHeight/2, whiteText );
mRenderCanvas.drawText( String.format( "%d %s", mMaxAxis, mUnits.getHeightUnit() ), 8, 15, whiteText );
}
private void drawSpeedsTexts()
{
mRenderCanvas.drawText( String.format( "%d %s", mMinAxis, mUnits.getSpeedUnit() ) , 8, mHeight, whiteText );
mRenderCanvas.drawText( String.format( "%d %s", (mMaxAxis+mMinAxis)/2, mUnits.getSpeedUnit() ) , 8, 3+mHeight/2, whiteText );
mRenderCanvas.drawText( String.format( "%d %s", mMaxAxis, mUnits.getSpeedUnit() ) , 8, 7+whiteText.getTextSize(), whiteText );
}
private void drawGraphType()
{
//float density = Resources.getSystem().getDisplayMetrics().density;
String text;
switch( mGraphType )
{
case( TIMESPEEDGRAPH ):
text = mContext.getResources().getString( R.string.graphtype_timespeed );
break;
case( DISTANCESPEEDGRAPH ):
text = mContext.getResources().getString( R.string.graphtype_distancespeed );
break;
case( TIMEALTITUDEGRAPH ):
text = mContext.getResources().getString( R.string.graphtype_timealtitude );
break;
case( DISTANCEALTITUDEGRAPH ):
text = mContext.getResources().getString( R.string.graphtype_distancealtitude );
break;
default:
text = "UNKNOWN GRAPH TYPE";
break;
}
mRenderCanvas.drawText( text, 5+mWidth/2, 5+mHeight/8, dkgrayLargeType );
}
private void drawTimeTexts()
{
DateFormat timeInstance = android.text.format.DateFormat.getTimeFormat(this.getContext().getApplicationContext());
String start = timeInstance.format( new Date( mStartTime ) );
String half = timeInstance.format( new Date( (mEndTime+mStartTime)/2 ) );
String end = timeInstance.format( new Date( mEndTime ) );
Path yAxis;
yAxis = new Path();
yAxis.moveTo( 5, 5+mHeight/2 );
yAxis.lineTo( 5, 5 );
mRenderCanvas.drawTextOnPath( String.format( start ), yAxis, 0, whiteCenteredText.getTextSize(), whiteCenteredText );
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, whiteCenteredText );
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, whiteCenteredText );
}
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() ) ;
Path yAxis;
yAxis = new Path();
yAxis.moveTo( 5, 5+mHeight/2 );
yAxis.lineTo( 5, 5 );
mRenderCanvas.drawTextOnPath( String.format( start ), yAxis, 0, whiteText.getTextSize(), whiteCenteredText );
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, whiteCenteredText );
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, whiteCenteredText );
}
private double translateValue( double val )
{
switch( mGraphType )
{
case( TIMESPEEDGRAPH ):
case( DISTANCESPEEDGRAPH ):
val = mUnits.conversionFromMetersPerSecond( val );
break;
case( TIMEALTITUDEGRAPH ):
case( DISTANCEALTITUDEGRAPH ):
val = mUnits.conversionFromMeterToHeight( val );
break;
default:
break;
}
return val;
}
private void drawGraph( double[][] values, int[][] valueDepth )
{
// Matrix
// Horizontals
mRenderCanvas.drawLine( 5, 5 , 5+mWidth, 5 , ltgreyMatrixDashed ); // top
mRenderCanvas.drawLine( 5, 5+mHeight/4 , 5+mWidth, 5+mHeight/4 , ltgreyMatrixDashed ); // 2nd
mRenderCanvas.drawLine( 5, 5+mHeight/2 , 5+mWidth, 5+mHeight/2 , ltgreyMatrixDashed ); // middle
mRenderCanvas.drawLine( 5, 5+mHeight/4*3, 5+mWidth, 5+mHeight/4*3, ltgreyMatrixDashed ); // 3rd
// Verticals
mRenderCanvas.drawLine( 5+mWidth/4 , 5, 5+mWidth/4 , 5+mHeight, ltgreyMatrixDashed ); // 2nd
mRenderCanvas.drawLine( 5+mWidth/2 , 5, 5+mWidth/2 , 5+mHeight, ltgreyMatrixDashed ); // middle
mRenderCanvas.drawLine( 5+mWidth/4*3, 5, 5+mWidth/4*3, 5+mHeight, ltgreyMatrixDashed ); // 3rd
mRenderCanvas.drawLine( 5+mWidth-1 , 5, 5+mWidth-1 , 5+mHeight, ltgreyMatrixDashed ); // right
// The line
Path mPath;
int emptyValues = 0;
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 )
{
if( emptyValues > mWidth/10 )
{
mPath.moveTo( (float)x+5, (float) y+5 );
}
else
{
mPath.lineTo( (float)x+5, (float) y+5 );
}
emptyValues = 0;
}
else
{
emptyValues++;
}
}
}
mRenderCanvas.drawPath( mPath, greenGraphLine );
// Axis's
mRenderCanvas.drawLine( 5, 5 , 5 , 5+mHeight, dkgreyMatrixLine );
mRenderCanvas.drawLine( 5, 5+mHeight, 5+mWidth, 5+mHeight, dkgreyMatrixLine );
}
}