package uk.org.smithfamily.mslogger.activity; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.regex.Pattern; import uk.org.smithfamily.mslogger.ApplicationSettings; import uk.org.smithfamily.mslogger.R; import uk.org.smithfamily.mslogger.chart.ChartFactory; import uk.org.smithfamily.mslogger.chart.GraphicalView; import uk.org.smithfamily.mslogger.chart.chart.PointStyle; import uk.org.smithfamily.mslogger.chart.model.TimeSeries; import uk.org.smithfamily.mslogger.chart.model.XYMultipleSeriesDataset; import uk.org.smithfamily.mslogger.chart.renderer.XYMultipleSeriesRenderer; import uk.org.smithfamily.mslogger.chart.renderer.XYSeriesRenderer; import uk.org.smithfamily.mslogger.log.DebugLogManager; import uk.org.smithfamily.mslogger.widgets.ScatterPlotZAxisGradient; import uk.org.smithfamily.mslogger.widgets.ZAxisGradient; import android.app.Activity; import android.app.ProgressDialog; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; import android.content.res.Resources; import android.graphics.Color; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup.LayoutParams; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TabHost; import android.widget.TabHost.OnTabChangeListener; import android.widget.TabHost.TabSpec; import android.widget.TextView; /** * Activity used to display a datalog file in a graph format to the user */ public class ViewDatalogActivity extends Activity { private GraphicalView mChartView; private GraphicalView mChartScatterPlotView; private readLogFileInBackground mReadlogAsync; private String[] headers; private String[] completeHeaders; private List<List<Double>> data; private List<List<Double>> dataScatterPlot; private TabHost tabHost; private Spinner xAxisSpinner; private Spinner yAxisSpinner; private Spinner zAxisSpinner; private Button btGenerate; private ScatterPlotZAxisGradient scatterPlotZAxisGradient; private Button selectDatalogFields; public static final int BACK_FROM_DATALOG_FIELDS = 1; /** * On creation of the activity, we bind click events and launch the datalog reading function in a different thread */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.viewdatalog); setTitle(R.string.datalog_viewer_title); tabHost = (TabHost) findViewById(R.id.tabhost); tabHost.setup(); Resources res = getResources(); TabSpec tabSpecLogViewer = tabHost.newTabSpec(getString(R.string.log_viewer)); tabSpecLogViewer.setContent(R.id.regular_datalog); tabSpecLogViewer.setIndicator(getString(R.string.log_viewer),res.getDrawable(R.drawable.logviewer)); TabSpec tabSpecScatterPlot = tabHost.newTabSpec(getString(R.string.scatter_plot)); tabSpecScatterPlot.setIndicator(getString(R.string.scatter_plot),res.getDrawable(R.drawable.scatterplot)); tabSpecScatterPlot.setContent(R.id.scatter_plot); tabHost.addTab(tabSpecLogViewer); tabHost.addTab(tabSpecScatterPlot); tabHost.setOnTabChangedListener(new OnTabChangeListener() { @Override public void onTabChanged(String tabId) { populateAxisSpinners(); } }); xAxisSpinner = (Spinner) findViewById(R.id.xAxisSpinner); yAxisSpinner = (Spinner) findViewById(R.id.yAxisSpinner); zAxisSpinner = (Spinner) findViewById(R.id.zAxisSpinner); scatterPlotZAxisGradient = (ScatterPlotZAxisGradient) findViewById(R.id.scatterPlotZAxisGradient); btGenerate = (Button) findViewById(R.id.btGenerate); btGenerate.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mReadlogAsync = (readLogFileInBackground) new readLogFileInBackground().execute("scatterplot"); } }); selectDatalogFields = (Button) findViewById(R.id.select_datalog_fields); selectDatalogFields.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Intent launchDatalogFields = new Intent(ViewDatalogActivity.this, DatalogFieldsActivity.class); Bundle b = new Bundle(); b.putStringArray("datalog_fields",completeHeaders); launchDatalogFields.putExtras(b); startActivityForResult(launchDatalogFields,BACK_FROM_DATALOG_FIELDS); } }); mReadlogAsync = (readLogFileInBackground) new readLogFileInBackground().execute("regular"); } private void populateAxisSpinners() { ArrayAdapter<String> datalogFieldsArrayAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, completeHeaders); // Specify the layout to use when the list of choices appears datalogFieldsArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); xAxisSpinner.setAdapter(datalogFieldsArrayAdapter); yAxisSpinner.setAdapter(datalogFieldsArrayAdapter); zAxisSpinner.setAdapter(datalogFieldsArrayAdapter); int i = 0; while (i < xAxisSpinner.getAdapter().getCount()) { if (datalogFieldsArrayAdapter.getItem(i).toString().equals("MAP")) { xAxisSpinner.setSelection(i); } if (datalogFieldsArrayAdapter.getItem(i).toString().equals("RPM")) { yAxisSpinner.setSelection(i); } if (datalogFieldsArrayAdapter.getItem(i).toString().equals("AFR")) { zAxisSpinner.setSelection(i); } i++; } } /** * Method called when the datalog fields have changed to refresh the graph */ protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == BACK_FROM_DATALOG_FIELDS) { LinearLayout layout = (LinearLayout) findViewById(R.id.chart); layout.removeAllViews(); mChartView = null; mReadlogAsync = (readLogFileInBackground) new readLogFileInBackground().execute("regular"); } } /** * Read the datalog and fill up all the necessary variables to generate a scatter plot * * @param datalog The datalog file name to read */ private void readScatterPlotDatalog(String datalog) { int[] indexOfFieldsToKeep = new int[3]; indexOfFieldsToKeep[0] = xAxisSpinner.getSelectedItemPosition(); indexOfFieldsToKeep[1] = yAxisSpinner.getSelectedItemPosition(); indexOfFieldsToKeep[2] = zAxisSpinner.getSelectedItemPosition(); dataScatterPlot = new ArrayList<List<Double>>(); for (int i = 0; i < 3; i++) { dataScatterPlot.add(new ArrayList<Double>()); } try { InputStream instream = new FileInputStream(datalog); if (instream != null) { try { // Prepare the file for reading InputStreamReader inputreader = new InputStreamReader(instream); BufferedReader buffreader = new BufferedReader(inputreader); int nbLine = 0; headers = new String[] {}; String line; String[] lineSplit; long timeStart = System.currentTimeMillis(); File datalogFile = new File(datalog); double currentLength = 0; double totalLength = datalogFile.length(); // Read every line of the file into the line-variable, on line at the time while ((line = buffreader.readLine()) != null) { if (mReadlogAsync.isCancelled()) break; if (nbLine > 1) { lineSplit = line.split("\t"); // Skip MARK and empty line if ((lineSplit[0].length() > 3 && lineSplit[0].substring(0,4).equals("MARK")) || lineSplit[0].equals("")) { } else { for (int i = 0; i < indexOfFieldsToKeep.length; i++) { double currentValue = 0; if (lineSplit.length > indexOfFieldsToKeep[i]) { currentValue = Double.parseDouble(lineSplit[indexOfFieldsToKeep[i]]); } dataScatterPlot.get(i).add(currentValue); } } } nbLine++; currentLength += line.length(); mReadlogAsync.doProgress((int) (currentLength * 100 / totalLength)); } buffreader.close(); long timeEnd = System.currentTimeMillis(); DebugLogManager.INSTANCE.log("Read datalog file in " + (timeEnd - timeStart) + " milliseconds",Log.DEBUG); } finally { instream.close(); } } } catch (FileNotFoundException e) { DebugLogManager.INSTANCE.logException(e); } catch (IOException e) { DebugLogManager.INSTANCE.logException(e); } } /** * Read the datalog and fill up all the necessary variables to generate a line chart * * @param datalog The datalog file name to read */ private void readRegularDatalog(String datalog) { String fieldsToKeep[] = ApplicationSettings.INSTANCE.getDatalogFields(); int indexOfFieldsToKeep[] = new int[fieldsToKeep.length]; try { InputStream instream = new FileInputStream(datalog); if (instream != null) { try { // Prepare the file for reading InputStreamReader inputreader = new InputStreamReader(instream); BufferedReader buffreader = new BufferedReader(inputreader); int nbLine = 0; headers = new String[] {}; data = new ArrayList<List<Double>>(); // Initialise list for (int i = 0; i < fieldsToKeep.length; i++) { data.add(new ArrayList<Double>()); } String line; String[] lineSplit; long timeStart = System.currentTimeMillis(); File datalogFile = new File(datalog); double currentLength = 0; double totalLength = datalogFile.length(); Pattern pattern = Pattern.compile("\t"); // Read every line of the file into the line-variable, on line at the time while ((line = buffreader.readLine()) != null) { if (mReadlogAsync.isCancelled()) break; if (nbLine > 0) { lineSplit = pattern.split(line); if (nbLine == 1) { headers = lineSplit; int k = 0; for (int i = 0; i < headers.length; i++) { for (int j = 0; j < fieldsToKeep.length; j++) { if (headers[i].equals(fieldsToKeep[j])) { indexOfFieldsToKeep[k++] = i; } } } completeHeaders = headers; headers = fieldsToKeep; } else { // Skip MARK and empty line if ((lineSplit[0].length() > 3 && lineSplit[0].substring(0,4).equals("MARK")) || lineSplit[0].equals("")) { } else { for (int i = 0; i < indexOfFieldsToKeep.length; i++) { double currentValue = 0; if (lineSplit.length > indexOfFieldsToKeep[i]) { currentValue = Double.parseDouble(lineSplit[indexOfFieldsToKeep[i]]); } data.get(i).add(currentValue); } } } } nbLine++; currentLength += line.length(); mReadlogAsync.doProgress((int) (currentLength * 100 / totalLength)); } buffreader.close(); long timeEnd = System.currentTimeMillis(); DebugLogManager.INSTANCE.log("Read datalog file in " + (timeEnd - timeStart) + " milliseconds",Log.DEBUG); } finally { instream.close(); } } } catch (FileNotFoundException e) { DebugLogManager.INSTANCE.logException(e); } catch (IOException e) { DebugLogManager.INSTANCE.logException(e); } } /** * Generate graph for the selected regular datalog */ private void generateRegularGraph() { double minXaxis = 0; double maxXaxis = data.get(0).size(); long timeStart = System.currentTimeMillis(); // Assuming first column of datalog is time for X axis double[] xValues = new double[(int)maxXaxis]; for (int i = 0; i < maxXaxis; i++) { xValues[i] = i; } // Rebuild the headers array for title, we use them all but the first one (Time) String[] titles = new String[headers.length - 1]; int[] colors = new int[headers.length - 1]; Random rand = new Random(); for (int i = 1; i < headers.length; i++) { titles[i - 1] = headers[i]; colors[i - 1] = Color.rgb(rand.nextInt(156) + 100, rand.nextInt(156) + 100, rand.nextInt(156) + 100); } List<double[]> x = new ArrayList<double[]>(); List<double[]> values = new ArrayList<double[]>(); // Add X values for all titles for (int i = 0; i < titles.length; i++) { x.add(xValues); } List<Double> minColumns = new ArrayList<Double>(); List<Double> maxColumns = new ArrayList<Double>(); // Find min and max value for each columns for (int i = 1; i < data.size(); i++) { List<Double> row = data.get(i); double min = row.get(0); double max = min; for (int j = 0; j < row.size(); j++) { double value = row.get(j); if (min > value) { min = value; } if (max < value) { max = value; } } minColumns.add(min); maxColumns.add(max); } long timeEnd = System.currentTimeMillis(); DebugLogManager.INSTANCE.log("Prepared value and found min/max value of each columns in " + (timeEnd - timeStart) + " milliseconds",Log.DEBUG); for (int i = 1; i < data.size(); i++) { List<Double> row = data.get(i); double[] rowDouble = new double[row.size()]; for (int j = 0; j < row.size(); j++) { rowDouble[j] = row.get(j); // Find percent between min and max rowDouble[j] = (rowDouble[j] - minColumns.get(i - 1)) / (maxColumns.get(i - 1) - minColumns.get(i - 1)) * 100; } values.add(rowDouble); } XYMultipleSeriesRenderer renderer = buildRenderer(titles.length, colors); setChartSettings(renderer, "", "", "", minXaxis, Math.min(100,maxXaxis), 0, 100, Color.GRAY, Color.LTGRAY); renderer.setPanLimits(new double[] { minXaxis,maxXaxis,0,100 }); renderer.setShowLabels(false); renderer.setClickEnabled(false); renderer.setShowGrid(true); renderer.setZoomEnabled(true); TextView currentlyViewing = (TextView) findViewById(R.id.currentlyViewing); Bundle b = getIntent().getExtras(); String datalog = b.getString("datalog"); currentlyViewing.setText("Currently viewing " + new File(datalog).getName()); LinearLayout layout = (LinearLayout) findViewById(R.id.chart); if (mChartView == null) { mChartView = ChartFactory.getLineChartView(ViewDatalogActivity.this, buildDateDataset(titles, x, values), renderer); /*mChartView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { SeriesSelection seriesSelection = mChartView.getCurrentSeriesAndPoint(); if (seriesSelection == null) { System.out.println("Nothing was clicked"); } else { System.out.println("Chart element data point index " + seriesSelection.getPointIndex() + " was clicked" + " point value=" + seriesSelection.getValue()); } } });*/ layout.addView(mChartView, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); } else { mChartView.repaint(); } } /** * Generate scatter plot for the current datalog */ public void generateScatterPlot() { String[] titles = new String[dataScatterPlot.get(0).size()]; int[] colors = new int[dataScatterPlot.get(0).size()]; List<double[]> x = new ArrayList<double[]>(); List<double[]> values = new ArrayList<double[]>(); List<Double> xAxis = dataScatterPlot.get(0); List<Double> yAxis = dataScatterPlot.get(1); //List<Double> zAxis = dataScatterPlot.get(2); for (int i = 0; i < xAxis.size(); i++) { x.add(new double[] { xAxis.get(i) }); values.add(new double[] { yAxis.get(i) }); } List<Double> minColumns = new ArrayList<Double>(); List<Double> maxColumns = new ArrayList<Double>(); // Find min and max value for each columns for (int i = 0; i < dataScatterPlot.size(); i++) { List<Double> row = dataScatterPlot.get(i); double min = row.get(0); double max = min; for (int j = 0; j < row.size(); j++) { double value = row.get(j); if (min > value) { min = value; } if (max < value) { max = value; } } minColumns.add(min); maxColumns.add(max); } scatterPlotZAxisGradient.initWithMinMax(minColumns.get(2), maxColumns.get(2)); ZAxisGradient color = new ZAxisGradient(minColumns.get(2), maxColumns.get(2)); for (int i = 0; i < dataScatterPlot.get(0).size(); i++) { titles[i] = ""; colors[i] = color.getColorForValue(dataScatterPlot.get(2).get(i)); } String xAxisField = xAxisSpinner.getSelectedItem().toString(); String yAxisField = yAxisSpinner.getSelectedItem().toString(); String title = xAxisSpinner.getSelectedItem().toString() + " vs " + yAxisField; XYMultipleSeriesRenderer renderer = buildRenderer(titles.length, colors); setChartSettings(renderer, title, xAxisField, yAxisField, minColumns.get(0) - 10, maxColumns.get(0) + 10, minColumns.get(1) - 10, maxColumns.get(1) + 10, Color.GRAY, Color.LTGRAY); renderer.setXLabels(10); renderer.setYLabels(10); renderer.setShowLegend(false); int length = renderer.getSeriesRendererCount(); for (int i = 0; i < length; i++) { ((XYSeriesRenderer) renderer.getSeriesRendererAt(i)).setFillPoints(true); } TextView chooseAxisAndGenerateText = (TextView) findViewById(R.id.chooseAxisAndGenerateText); chooseAxisAndGenerateText.setVisibility(View.GONE); LinearLayout scatterPlotBottom = (LinearLayout) findViewById(R.id.scatterPlotBottom); scatterPlotBottom.setVisibility(View.VISIBLE); LinearLayout layout = (LinearLayout) findViewById(R.id.chart_scatter_plot); if (mChartScatterPlotView == null) { mChartScatterPlotView = ChartFactory.getScatterChartView(ViewDatalogActivity.this, buildDateDataset(titles, x, values), renderer); layout.addView(mChartScatterPlotView, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); } else { mChartScatterPlotView.repaint(); } } /** * Builds an XY multiple time dataset using the provided values. * * @param titles * the series titles * @param xValues * the values for the X axis * @param yValues * the values for the Y axis * @return the XY multiple time dataset */ protected XYMultipleSeriesDataset buildDateDataset(String[] titles, List<double[]> xValues, List<double[]> yValues) { XYMultipleSeriesDataset dataset = new XYMultipleSeriesDataset(); int length = titles.length; for (int i = 0; i < length; i++) { TimeSeries series = new TimeSeries(titles[i]); double[] xV = xValues.get(i); double[] yV = yValues.get(i); int seriesLength = xV.length; for (int k = 0; k < seriesLength; k++) { series.add(xV[k], yV[k]); } dataset.addSeries(series); } return dataset; } /** * Builds an XY multiple series renderer. * * @param nbLines Number of lines * @param colors Array of colors for each point * @return The XY multiple series renderers */ protected XYMultipleSeriesRenderer buildRenderer(int nbLines, int[] colors) { XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer(); setRenderer(renderer, nbLines, colors); return renderer; } protected void setRenderer(XYMultipleSeriesRenderer renderer, int nbLines, int[] colors) { renderer.setAxisTitleTextSize(16); renderer.setChartTitleTextSize(20); renderer.setLabelsTextSize(15); renderer.setLegendTextSize(15); renderer.setPointSize(5f); renderer.setMargins(new int[] { 20, 30, 15, 20 }); for (int i = 0; i < nbLines; i++) { XYSeriesRenderer r = new XYSeriesRenderer(); r.setColor(colors[i]); r.setPointStyle(PointStyle.POINT); renderer.addSeriesRenderer(r); } } /** * Sets a few of the series renderer settings. * * @param renderer * the renderer to set the properties to * @param title * the chart title * @param xTitle * the title for the X axis * @param yTitle * the title for the Y axis * @param xMin * the minimum value on the X axis * @param xMax * the maximum value on the X axis * @param yMin * the minimum value on the Y axis * @param yMax * the maximum value on the Y axis * @param axesColor * the axes color * @param labelsColor * the labels color */ protected void setChartSettings(XYMultipleSeriesRenderer renderer, String title, String xTitle, String yTitle, double xMin, double xMax, double yMin, double yMax, int axesColor, int labelsColor) { renderer.setChartTitle(title); renderer.setXTitle(xTitle); renderer.setYTitle(yTitle); renderer.setXAxisMin(xMin); renderer.setXAxisMax(xMax); renderer.setYAxisMin(yMin); renderer.setYAxisMax(yMax); renderer.setAxesColor(axesColor); renderer.setLabelsColor(labelsColor); } /** * AsyncTask that is used to read datalog in a background task while the UI can keep updating */ private class readLogFileInBackground extends AsyncTask<String, Integer, Void> { private ProgressDialog dialog = new ProgressDialog(ViewDatalogActivity.this); private long taskStartTime; private long lastRemainingUpdate; private String graphType = ""; /** * This is executed before doInBackground */ protected void onPreExecute() { taskStartTime = System.currentTimeMillis(); dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); dialog.setProgress(0); dialog.setMessage("Reading datalog..."); dialog.show(); // Finish "View datalog" activity when canceling dialog.setOnCancelListener(new OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { mReadlogAsync.cancel(true); finish(); } }); } /** * @param result This is executed after doInBackground and the result is returned in result */ @Override protected void onPostExecute(Void result) { super.onPostExecute(result); if (graphType.equals("regular")) { generateRegularGraph(); } else if (graphType.equals("scatterplot")) { generateScatterPlot(); } dialog.dismiss(); } /** * Called by the UI thread to update progress * @param value The new value of the progress bar */ public void doProgress(int value) { publishProgress(value); } /** * @param value The new value of the progress bar */ protected void onProgressUpdate(Integer... value) { super.onProgressUpdate(value); long currentTime = System.currentTimeMillis(); long elapsedMillis = (currentTime - taskStartTime); int percentValue = value[0]; long totalMillis = (long) (elapsedMillis / (((double) percentValue) / 100.0)); long remainingMillis = totalMillis - elapsedMillis; int remainingSeconds = (int) remainingMillis / 1000; /* Update the status string. If task is less than 5% complete or started less then 2 seconds ago, assume that the estimate is inaccurate Also, don't update more often then every second */ if (percentValue >= 5 && elapsedMillis > 2000 && currentTime - lastRemainingUpdate > 1000) { dialog.setMessage("Reading datalog (About " + remainingSeconds + " second(s) remaining)..."); lastRemainingUpdate = System.currentTimeMillis(); } dialog.setProgress(percentValue); } /** * This is the main function that is executed in another thread * * @param params Parameters of the task */ @Override protected Void doInBackground(String... params) { graphType = params[0]; Bundle b = getIntent().getExtras(); String datalog = b.getString("datalog"); if (graphType.equals("regular")) { readRegularDatalog(datalog); } else if (graphType.equals("scatterplot")) { readScatterPlotDatalog(datalog); } return null; } } }