package com.openvehicles.OVMS.ui; import android.app.AlertDialog; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.os.Bundle; import android.os.Handler; import android.text.Html; import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; import com.github.mikephil.charting.charts.BarLineChartBase; import com.github.mikephil.charting.charts.Chart; import com.github.mikephil.charting.charts.LineChart; import com.github.mikephil.charting.components.LimitLine; import com.github.mikephil.charting.components.XAxis; import com.github.mikephil.charting.components.YAxis; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.LineData; import com.github.mikephil.charting.data.LineDataSet; import com.github.mikephil.charting.listener.OnChartGestureListener; import com.github.mikephil.charting.utils.ValueFormatter; import com.luttu.AppPrefes; import com.openvehicles.OVMS.R; import com.openvehicles.OVMS.entities.CarData; import com.openvehicles.OVMS.entities.CmdSeries; import com.openvehicles.OVMS.entities.GPSLogData; import com.openvehicles.OVMS.ui.utils.ProgressOverlay; import com.openvehicles.OVMS.utils.CarsStorage; import java.text.SimpleDateFormat; import java.util.ArrayList; /** * Created by balzer on 06.04.15. */ public class PowerFragment extends BaseFragment implements CmdSeries.Listener, ProgressOverlay.OnCancelListener { private static final String TAG = "PowerFragment"; // data set colors: private static final int COLOR_ALTITUDE = Color.parseColor("#C0C8AB37"); private static final int COLOR_ALTITUDE_GRID = Color.parseColor("#80C8AB37"); private static final int COLOR_SPEED = Color.parseColor("#FF7777FF"); private static final int COLOR_SPEED_GRID = Color.parseColor("#807777FF"); private static final int COLOR_POWER_MIN = Color.parseColor("#FF999999"); private static final int COLOR_POWER_MAX = Color.parseColor("#FF999999"); private static final int COLOR_POWER_AVG = Color.parseColor("#FFFF0000"); private static final int COLOR_ENERGY_USED = Color.parseColor("#FF999999"); private static final int COLOR_ENERGY_RECD = Color.parseColor("#FF999999"); private static final int COLOR_ENERGY_AVG = Color.parseColor("#FFFFFF00"); // data storage: private GPSLogData logData; private ArrayList<GPSLogData.Entry> chartEntries; // user interface: private Menu optionsMenu; private LineChart tripChart; private LineChart powerChart; private LineChart energyChart; private CoupleChartGestureListener chartCoupler; private boolean mShowPower = true; private boolean mShowEnergy = true; // system services: private final static Handler mHandler = new Handler(); private CarData mCarData; private CmdSeries cmdSeries; private AppPrefes appPrefes; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Init data storage: logData = new GPSLogData(); // Load prefs: appPrefes = new AppPrefes(getActivity(), "ovms"); mShowPower = appPrefes.getData("power_show_power").equals("on"); mShowEnergy = appPrefes.getData("power_show_energy").equals("on"); if (!mShowPower && !mShowEnergy) mShowPower = true; // Setup UI: ProgressOverlay progressOverlay = createProgressOverlay(inflater, container, false); progressOverlay.setOnCancelListener(this); View rootView = inflater.inflate(R.layout.fragment_power, null); XAxis xAxis; YAxis yAxis; LineChart chart; // // Setup trip chart: // tripChart = chart = (LineChart) rootView.findViewById(R.id.chart_trip); chart.setDescription(getString(R.string.power_trip_description)); chart.getPaint(LineChart.PAINT_DESCRIPTION).setColor(Color.LTGRAY); chart.setDrawGridBackground(false); chart.setDrawBorders(true); chart.setHighlightEnabled(false); xAxis = chart.getXAxis(); xAxis.setTextColor(Color.WHITE); yAxis = chart.getAxisLeft(); // altitude yAxis.setTextColor(COLOR_ALTITUDE); yAxis.setGridColor(COLOR_ALTITUDE_GRID); yAxis.setStartAtZero(false); yAxis.setPosition(YAxis.YAxisLabelPosition.INSIDE_CHART); yAxis = chart.getAxisRight(); // speed yAxis.setTextColor(COLOR_SPEED); yAxis.setGridColor(COLOR_SPEED_GRID); yAxis.setStartAtZero(true); yAxis.setPosition(YAxis.YAxisLabelPosition.INSIDE_CHART); // // Setup power chart: // powerChart = chart = (LineChart) rootView.findViewById(R.id.chart_power); chart.setDescription(getString(R.string.power_power_description)); chart.getPaint(LineChart.PAINT_DESCRIPTION).setColor(Color.LTGRAY); chart.setDrawGridBackground(false); chart.setDrawBorders(true); chart.setHighlightEnabled(false); xAxis = chart.getXAxis(); xAxis.setTextColor(Color.WHITE); yAxis = chart.getAxisLeft(); yAxis.setTextColor(Color.WHITE); yAxis.setGridColor(Color.LTGRAY); yAxis.setStartAtZero(false); yAxis.setPosition(YAxis.YAxisLabelPosition.INSIDE_CHART); yAxis.setValueFormatter(new ValueFormatter() { @Override public String getFormattedValue(float value) { return String.format("%.0fkW", value / 1000f); } }); yAxis = chart.getAxisRight(); yAxis.setTextColor(Color.WHITE); yAxis.setGridColor(Color.LTGRAY); yAxis.setStartAtZero(false); yAxis.setPosition(YAxis.YAxisLabelPosition.INSIDE_CHART); yAxis.setValueFormatter(new ValueFormatter() { @Override public String getFormattedValue(float value) { return String.format("%.0fkW", value / 1000f); } }); // // Setup energy chart: // energyChart = chart = (LineChart) rootView.findViewById(R.id.chart_energy); chart.setDescription(getString(R.string.power_energy_description)); chart.getPaint(LineChart.PAINT_DESCRIPTION).setColor(Color.LTGRAY); chart.setDrawGridBackground(false); chart.setDrawBorders(true); chart.setHighlightEnabled(false); xAxis = chart.getXAxis(); xAxis.setTextColor(Color.WHITE); yAxis = chart.getAxisLeft(); yAxis.setTextColor(Color.WHITE); yAxis.setGridColor(Color.LTGRAY); yAxis.setStartAtZero(false); yAxis.setPosition(YAxis.YAxisLabelPosition.INSIDE_CHART); yAxis = chart.getAxisRight(); yAxis.setTextColor(Color.WHITE); yAxis.setGridColor(Color.LTGRAY); yAxis.setStartAtZero(false); yAxis.setPosition(YAxis.YAxisLabelPosition.INSIDE_CHART); // // Couple chart viewports: // tripChart.setOnChartGestureListener(chartCoupler = new CoupleChartGestureListener( tripChart, new Chart[] { powerChart, energyChart })); powerChart.setOnChartGestureListener(new CoupleChartGestureListener( powerChart, new Chart[] { tripChart, energyChart })); energyChart.setOnChartGestureListener(new CoupleChartGestureListener( energyChart, new Chart[] { tripChart, powerChart })); // attach menu: setHasOptionsMenu(true); return rootView; } public class CoupleChartGestureListener implements OnChartGestureListener { private Chart srcChart; private Chart[] dstCharts; public CoupleChartGestureListener(Chart srcChart, Chart[] dstCharts) { this.srcChart = srcChart; this.dstCharts = dstCharts; } @Override public void onChartLongPressed(MotionEvent me) { // nop } @Override public void onChartDoubleTapped(MotionEvent me) { // nop } @Override public void onChartSingleTapped(MotionEvent me) { // nop } @Override public void onChartFling(MotionEvent me1, MotionEvent me2, float velocityX, float velocityY) { // nop } @Override public void onChartScale(MotionEvent me, float scaleX, float scaleY) { //Log.d(TAG, "onChartScale " + scaleX + "/" + scaleY + " X=" + me.getX() + "Y=" + me.getY()); syncCharts(); } @Override public void onChartTranslate(MotionEvent me, float dX, float dY) { //Log.d(TAG, "onChartTranslate " + dX + "/" + dY + " X=" + me.getX() + "Y=" + me.getY()); syncCharts(); } public void syncCharts() { Matrix srcMatrix; float[] srcVals = new float[9]; Matrix dstMatrix; float[] dstVals = new float[9]; // get src chart translation matrix: srcMatrix = srcChart.getViewPortHandler().getMatrixTouch(); srcMatrix.getValues(srcVals); // apply X axis scaling and position to dst charts: for (Chart dstChart : dstCharts) { if (dstChart.getVisibility() == View.VISIBLE) { dstMatrix = dstChart.getViewPortHandler().getMatrixTouch(); dstMatrix.getValues(dstVals); dstVals[Matrix.MSCALE_X] = srcVals[Matrix.MSCALE_X]; dstVals[Matrix.MTRANS_X] = srcVals[Matrix.MTRANS_X]; dstMatrix.setValues(dstVals); dstChart.getViewPortHandler().refresh(dstMatrix, dstChart, true); } } } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.power_chart_options, menu); optionsMenu = menu; // configure checkbox items: optionsMenu.findItem(R.id.mi_chk_power).setChecked(mShowPower); optionsMenu.findItem(R.id.mi_chk_energy).setChecked(mShowEnergy); } @Override public void onPrepareOptionsMenu(Menu menu) { if (mCarData != null) { // insert user distance units into menu titles: optionsMenu.findItem(R.id.mi_power_zoom5).setTitle( getString(R.string.power_btn_zoom5, mCarData.car_distance_units)); optionsMenu.findItem(R.id.mi_power_zoom10).setTitle( getString(R.string.power_btn_zoom10, mCarData.car_distance_units)); } } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); getSherlockActivity().setTitle(R.string.power_title); getSherlockActivity().getSupportActionBar().setIcon(R.drawable.ic_action_chart); // get data of current car: mCarData = CarsStorage.get().getSelectedCarData(); getSherlockActivity().invalidateOptionsMenu(); // schedule data loader: showProgressOverlay(getString(R.string.power_msg_loading_data)); mHandler.postDelayed(new Runnable() { @Override public void run() { // load and display saved vehicle data: GPSLogData saved = GPSLogData.loadFile(mCarData.sel_vehicleid); if (saved != null) { Log.v(TAG, "GPSLogData loaded for " + mCarData.sel_vehicleid); logData = saved; dataSetChanged(); } hideProgressOverlay(); } }, 200); } @Override public boolean onOptionsItemSelected(MenuItem item) { int menuId = item.getItemId(); boolean newState = !item.isChecked(); switch (menuId) { case R.id.mi_get_data: cmdSeries = new CmdSeries(getService(), this) .add(R.string.power_msg_getting_gpslog, "32,RT-GPS-Log") .start(); return true; case R.id.mi_power_zoom5: zoomOdometerRange(5); return true; case R.id.mi_power_zoom10: zoomOdometerRange(10); return true; case R.id.mi_reset_view: tripChart.fitScreen(); powerChart.fitScreen(); energyChart.fitScreen(); return true; case R.id.mi_help: new AlertDialog.Builder(getActivity()) .setTitle(R.string.power_btn_help) .setMessage(Html.fromHtml(getString(R.string.power_help))) .setPositiveButton(android.R.string.ok, null) .show(); return true; case R.id.mi_chk_power: mShowPower = newState; if (!mShowPower && !mShowEnergy) { mShowEnergy = true; optionsMenu.findItem(R.id.mi_chk_energy).setChecked(true); } item.setChecked(newState); dataFilterChanged(); return true; case R.id.mi_chk_energy: mShowEnergy = newState; if (!mShowPower && !mShowEnergy) { mShowPower = true; optionsMenu.findItem(R.id.mi_chk_power).setChecked(true); } item.setChecked(newState); dataFilterChanged(); return true; } return super.onOptionsItemSelected(item); } @Override public void onStop() { if (cmdSeries != null) cmdSeries.cancel(); super.onStop(); } @Override public void onCmdSeriesProgress(String message, int pos, int posCnt, int step, int stepCnt) { stepProgressOverlay(message, pos, posCnt, step, stepCnt); } @Override public void onProgressCancel() { if (cmdSeries != null) cmdSeries.cancel(); hideProgressOverlay(); } @Override public void onCmdSeriesFinish(CmdSeries cmdSeries, int returnCode) { String errorMsg = ""; String errorDetail = ""; hideProgressOverlay(); switch (returnCode) { case -1: // abort return; case 0: // ok: process & display results: showProgressOverlay(getString(R.string.msg_processing_data)); logData.processCmdResults(cmdSeries); logData.saveFile(mCarData.sel_vehicleid); dataSetChanged(); hideProgressOverlay(); return; case 1: // failed errorDetail = cmdSeries.getErrorDetail(); if (errorDetail.contains("B ")) errorDetail += getString(R.string.hint_sevcon_offline); errorMsg = getString(R.string.err_failed, errorDetail); break; case 2: // unsupported errorMsg = getString(R.string.err_unsupported_operation); break; case 3: // unimplemented errorMsg = getString(R.string.err_unimplemented_operation); break; } // getting here means error: new AlertDialog.Builder(getActivity()) .setTitle(R.string.Error) .setMessage(cmdSeries.getMessage() + " => " + errorMsg) .setPositiveButton(android.R.string.ok, null) .show(); } /** * Check data model validity */ private boolean isPackValid() { return (logData != null && logData.entries != null && logData.entries.size() > 1); } /** * Call when underlying batteryData object ready/changed */ public void dataSetChanged() { if (!isPackValid()) return; // update charts: updateCharts(); } /** * Call when user changed data filter (i.e. show flags) */ private void dataFilterChanged() { // save prefs: appPrefes.SaveData("power_show_power", mShowPower ? "on" : "off"); appPrefes.SaveData("power_show_energy", mShowEnergy ? "on" : "off"); // check data status: if (!isPackValid()) return; // update charts: updateCharts(); // sync viewports: chartCoupler.syncCharts(); } /** * Update the charts */ public void updateCharts() { if (!isPackValid()) return; ArrayList<GPSLogData.Entry> logEntries = logData.entries; GPSLogData.Entry entry, refEntry; int refIndex; SimpleDateFormat timeFmt = new SimpleDateFormat("HH:mm"); // // Create value arrays: // chartEntries = new ArrayList<GPSLogData.Entry>(); ArrayList<String> xValues = new ArrayList<String>(); ArrayList<LimitLine> xSections = new ArrayList<LimitLine>(); ArrayList<Entry> altValues = new ArrayList<Entry>(); ArrayList<Entry> spdValues = new ArrayList<Entry>(); ArrayList<Entry> pwrMinValues = new ArrayList<Entry>(); ArrayList<Entry> pwrMaxValues = new ArrayList<Entry>(); ArrayList<Entry> pwrAvgValues = new ArrayList<Entry>(); ArrayList<Entry> energyUseValues = new ArrayList<Entry>(); ArrayList<Entry> energyRecValues = new ArrayList<Entry>(); ArrayList<Entry> energyAvgValues = new ArrayList<Entry>(); String units = mCarData.car_distance_units_raw; String xLabel; int xIndex = 0; refIndex = 0; refEntry = logEntries.get(0); for (int i = 1; i < logEntries.size(); i++) { entry = logEntries.get(i); // check data distance from ref: if (entry.isNewTimePoint(refEntry)) { // X axis to log entry association: chartEntries.add(entry); //xLabel = String.format("%.0f", entry.getOdometer(units)); xLabel = timeFmt.format(entry.timeStamp); xValues.add(xLabel); altValues.add(new Entry(entry.altitude, xIndex)); spdValues.add(new Entry(entry.getSpeed(refEntry, units), xIndex)); pwrMinValues.add(new Entry(entry.getMinPower(logEntries, refEntry), xIndex)); pwrMaxValues.add(new Entry(entry.getMaxPower(logEntries, refEntry), xIndex)); pwrAvgValues.add(new Entry(entry.getSegAvgPwr(refEntry), xIndex)); energyUseValues.add(new Entry(entry.getSegUsedWh(refEntry), xIndex)); energyRecValues.add(new Entry(entry.getSegRecdWh(refEntry), xIndex)); energyAvgValues.add(new Entry(entry.getSegAvgEnergy(refEntry, units), xIndex)); // add section markers: if (entry.isSectionStart(refEntry)) { LimitLine l = new LimitLine(xIndex); l.setLabel(String.format("%.0f", entry.getOdometer(units))); l.setLabelPosition(LimitLine.LimitLabelPosition.POS_RIGHT); l.setTextColor(Color.WHITE); l.setTextStyle(Paint.Style.FILL); l.enableDashedLine(3f, 2f, 0f); xSections.add(l); } xIndex++; // advance reference point: while (entry.isNewTimePoint(refEntry) && refIndex <= i) refEntry = logEntries.get(++refIndex); } } // // Update trip chart: // ArrayList<LineDataSet> dataSets = new ArrayList<LineDataSet>(); LineDataSet dataSet; dataSet = new LineDataSet(altValues, getString(R.string.power_data_altitude)); dataSet.setAxisDependency(YAxis.AxisDependency.LEFT); dataSet.setColor(COLOR_ALTITUDE); dataSet.setDrawFilled(true); dataSet.setLineWidth(2f); dataSet.setDrawCircles(false); dataSet.setDrawValues(false); dataSet.setDrawCubic(true); dataSets.add(dataSet); dataSet = new LineDataSet(spdValues, getString(R.string.power_data_speed, mCarData.car_speed_units)); dataSet.setAxisDependency(YAxis.AxisDependency.RIGHT); dataSet.setColor(COLOR_SPEED); dataSet.setLineWidth(4f); dataSet.setDrawCircles(false); dataSet.setDrawValues(false); dataSet.setDrawCubic(true); dataSets.add(dataSet); // display data sets: LineData data; data = new LineData(xValues, dataSets); data.setValueTextColor(Color.WHITE); data.setValueTextSize(9f); tripChart.setData(data); XAxis xAxis = tripChart.getXAxis(); xAxis.removeAllLimitLines(); for (int i = 0; i < xSections.size(); i++) { xAxis.addLimitLine(xSections.get(i)); } tripChart.getLegend().setTextColor(Color.WHITE); tripChart.invalidate(); // // Update power chart: // if (!mShowPower) { powerChart.setVisibility(View.GONE); powerChart.clear(); } else { powerChart.setVisibility(View.VISIBLE); dataSets = new ArrayList<LineDataSet>(); dataSet = new LineDataSet(pwrMinValues, getString(R.string.power_data_pwr_min)); dataSet.setAxisDependency(YAxis.AxisDependency.LEFT); dataSet.setColor(COLOR_POWER_MIN); dataSet.setDrawFilled(true); dataSet.setLineWidth(1f); dataSet.setDrawCircles(false); dataSet.setDrawValues(false); dataSet.setDrawCubic(true); dataSets.add(dataSet); dataSet = new LineDataSet(pwrMaxValues, getString(R.string.power_data_pwr_max)); dataSet.setAxisDependency(YAxis.AxisDependency.LEFT); dataSet.setColor(COLOR_POWER_MAX); dataSet.setDrawFilled(true); dataSet.setLineWidth(1f); dataSet.setDrawCircles(false); dataSet.setDrawValues(false); dataSet.setDrawCubic(true); dataSets.add(dataSet); dataSet = new LineDataSet(pwrAvgValues, getString(R.string.power_data_pwr_avg)); dataSet.setAxisDependency(YAxis.AxisDependency.LEFT); dataSet.setColor(COLOR_POWER_AVG); dataSet.setDrawFilled(false); dataSet.setLineWidth(4f); dataSet.setDrawCircles(false); dataSet.setDrawValues(false); dataSet.setDrawCubic(true); dataSets.add(dataSet); // display data sets: data = new LineData(xValues, dataSets); data.setValueTextColor(Color.WHITE); data.setValueTextSize(9f); powerChart.setData(data); xAxis = powerChart.getXAxis(); xAxis.removeAllLimitLines(); for (int i = 0; i < xSections.size(); i++) { xAxis.addLimitLine(xSections.get(i)); } powerChart.getLegend().setTextColor(Color.WHITE); powerChart.invalidate(); } // // Update energy chart: // if (!mShowEnergy) { energyChart.setVisibility(View.GONE); energyChart.clear(); } else { energyChart.setVisibility(View.VISIBLE); dataSets = new ArrayList<LineDataSet>(); dataSet = new LineDataSet(energyUseValues, getString(R.string.power_data_energy_used)); dataSet.setAxisDependency(YAxis.AxisDependency.LEFT); dataSet.setColor(COLOR_ENERGY_USED); dataSet.setDrawFilled(true); dataSet.setLineWidth(1f); dataSet.setDrawCircles(false); dataSet.setDrawValues(false); dataSet.setDrawCubic(true); dataSets.add(dataSet); dataSet = new LineDataSet(energyRecValues, getString(R.string.power_data_energy_recd)); dataSet.setAxisDependency(YAxis.AxisDependency.LEFT); dataSet.setColor(COLOR_ENERGY_RECD); dataSet.setDrawFilled(true); dataSet.setLineWidth(1f); dataSet.setDrawCircles(false); dataSet.setDrawValues(false); dataSet.setDrawCubic(true); dataSets.add(dataSet); dataSet = new LineDataSet(energyAvgValues, getString(R.string.power_data_energy_avg, mCarData.car_distance_units)); dataSet.setAxisDependency(YAxis.AxisDependency.LEFT); dataSet.setColor(COLOR_ENERGY_AVG); dataSet.setDrawFilled(false); dataSet.setLineWidth(4f); dataSet.setDrawCircles(false); dataSet.setDrawValues(false); dataSet.setDrawCubic(true); dataSets.add(dataSet); // display data sets: data = new LineData(xValues, dataSets); data.setValueTextColor(Color.WHITE); data.setValueTextSize(9f); energyChart.setData(data); xAxis = energyChart.getXAxis(); xAxis.removeAllLimitLines(); for (int i = 0; i < xSections.size(); i++) { xAxis.addLimitLine(xSections.get(i)); } energyChart.getLegend().setTextColor(Color.WHITE); energyChart.invalidate(); } } /** * Zoom to odometer range * * @param size -- size of range in user units (km/mi) */ private void zoomOdometerRange(int size) { int start = 0, end = chartEntries.size()-1; GPSLogData.Entry entry; float odoEnd = 0; // find last drive entry: for (int i = chartEntries.size()-1; i >= 0; i--) { entry = chartEntries.get(i); if (entry.getOpStatus() == 1) { odoEnd = entry.getOdometer(mCarData.car_distance_units_raw); end = i; break; } } // find entry for (odoEnd - size): for (int i = end-1; i >= 0; i--) { entry = chartEntries.get(i); if (entry.getOdometer(mCarData.car_distance_units_raw) <= (odoEnd - size)) { start = i; break; } } // zoom charts: float scaleX = (float) chartEntries.size() / (end - start + 1); for (BarLineChartBase chart : new BarLineChartBase[] { tripChart, powerChart, energyChart } ) { chart.fitScreen(); chart.zoom(scaleX, 1f, chart.getWidth() / 2f, chart.getHeight() / 2f); chart.moveViewToX(start); } } }