package com.evancharlton.mileage;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.LinearLayout;
import android.widget.SimpleCursorAdapter;
import android.widget.Spinner;
import com.evancharlton.mileage.binders.VehicleBinder;
import com.evancharlton.mileage.calculators.CalculationEngine;
import com.evancharlton.mileage.models.FillUp;
import com.evancharlton.mileage.models.Statistic;
import com.evancharlton.mileage.models.StatisticsGroup;
import com.evancharlton.mileage.models.Vehicle;
public class StatisticsView extends TabChildActivity {
private Spinner m_vehicles;
private PreferencesProvider m_preferences;
private CalculationEngine m_calcEngine;
private static final int DIALOG_STATS_PROGRESS = 1;
private ProgressDialog m_dlg = null;
private CalculationTask m_calculationTask;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.statistics);
m_calculationTask = (CalculationTask) getLastNonConfigurationInstance();
if (m_calculationTask == null) {
m_calculationTask = new CalculationTask();
}
m_calculationTask.activity = this;
}
@Override
public Object onRetainNonConfigurationInstance() {
// FIXME: This is horrible. Why am I recalculating on screen rotation?
return m_calculationTask.getStatus() == AsyncTask.Status.FINISHED ? null : m_calculationTask;
}
public void onResume() {
super.onResume();
m_preferences = PreferencesProvider.getInstance(this);
m_calcEngine = m_preferences.getCalculator();
initUI();
populateSpinner();
}
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
Mileage.createMenu(menu);
HelpDialog.injectHelp(menu, 'h');
return true;
}
public boolean onOptionsItemSelected(MenuItem item) {
boolean ret = Mileage.parseMenuItem(item, this);
if (ret) {
return true;
}
switch (item.getItemId()) {
case HelpDialog.MENU_HELP:
HelpDialog.create(this, R.string.help_title_statistics, R.string.help_statistics);
break;
}
return super.onOptionsItemSelected(item);
}
private void initUI() {
m_vehicles = (Spinner) findViewById(R.id.stats_vehicle_spinner);
}
private void populateSpinner() {
Cursor c = managedQuery(Vehicle.CONTENT_URI, Vehicle.getProjection(), null, null, Vehicle.DEFAULT_SORT_ORDER);
SimpleCursorAdapter vehicleAdapter = new SimpleCursorAdapter(this, android.R.layout.simple_spinner_item, c, new String[] {
Vehicle.TITLE
}, new int[] {
android.R.id.text1
});
vehicleAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
vehicleAdapter.setViewBinder(new VehicleBinder());
m_vehicles.setAdapter(vehicleAdapter);
setVehicleSelection(m_vehicles);
m_vehicles.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
public void onItemSelected(AdapterView<?> adapter, View view, int position, long id) {
updateVehicleSelection(position);
calculateStatistics(m_vehicles.getSelectedItemId());
}
public void onNothingSelected(AdapterView<?> arg0) {
}
});
if (vehicleAdapter.getCount() == 1) {
m_vehicles.setVisibility(View.GONE);
calculateStatistics(vehicleAdapter.getItemId(0));
}
}
private void calculateStatistics(final long id) {
LinearLayout container = (LinearLayout) findViewById(R.id.stats_container);
container.removeAllViews();
if (m_calculationTask.getStatus() == AsyncTask.Status.PENDING) {
m_calculationTask.execute(id);
} else if (m_calculationTask.getStatus() == AsyncTask.Status.RUNNING) {
showDialog(DIALOG_STATS_PROGRESS);
}
}
@Override
protected Dialog onCreateDialog(int which) {
switch (which) {
case DIALOG_STATS_PROGRESS:
m_dlg = new ProgressDialog(this);
m_dlg.setTitle(getString(R.string.calculating));
m_dlg.setMessage(getString(R.string.statistics_calculating));
m_dlg.setIndeterminate(false);
m_dlg.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
m_dlg.setProgress(0);
m_dlg.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
if (m_calculationTask != null && m_calculationTask.getStatus().equals(AsyncTask.Status.RUNNING)) {
m_calculationTask.cancel(true);
}
}
});
return m_dlg;
}
return null;
}
public StatisticsGroup calcDistances(final List<FillUp> fillups) {
StatisticsGroup group = new StatisticsGroup(getString(R.string.distance_between_fillups));
double total_distance = 0D;
double min_distance = Double.MAX_VALUE;
double max_distance = 0D;
for (FillUp fillup : fillups) {
double distance = fillup.calcDistance();
if (distance > 0) {
total_distance += distance;
if (distance < min_distance) {
min_distance = distance;
}
if (distance > max_distance) {
max_distance = distance;
}
}
}
group.add(new Statistic(getString(R.string.average), (total_distance / (fillups.size() - 1)), m_calcEngine.getDistanceUnitsAbbr()));
group.add(new Statistic(getString(R.string.maximum), max_distance, m_calcEngine.getDistanceUnitsAbbr()));
group.add(new Statistic(getString(R.string.minimum), min_distance, m_calcEngine.getDistanceUnitsAbbr()));
group.add(new Statistic(getString(R.string.last), fillups.get(fillups.size() - 1).calcDistance(), m_calcEngine.getDistanceUnitsAbbr()));
return group;
}
public StatisticsGroup calcEconomy(final List<FillUp> fillups) {
StatisticsGroup group = new StatisticsGroup(getString(R.string.fuel_economy));
double total_distance = fillups.get(fillups.size() - 1).getOdometer() - fillups.get(0).getOdometer();
double total_fuel = 0D;
for (int i = fillups.size() - 1; i > 0; i--) {
total_fuel += fillups.get(i).getAmount();
}
double max_economy = 0D;
double min_economy = Double.MAX_VALUE;
if (m_calcEngine.isInverted()) {
max_economy = Double.MAX_VALUE;
min_economy = 0D;
}
for (FillUp fillup : fillups) {
if (!fillup.isPartial()) {
double economy = fillup.calcEconomy();
if (economy < 0) {
continue;
}
if (economy > 0) {
if (m_calcEngine.better(economy, max_economy)) {
max_economy = economy;
}
if (m_calcEngine.worse(economy, min_economy)) {
min_economy = economy;
}
}
}
}
group.add(new Statistic(getString(R.string.average), m_calcEngine.calculateEconomy(total_distance, total_fuel), m_calcEngine.getEconomyUnits()));
group.add(new Statistic("Best", max_economy, m_calcEngine.getEconomyUnits()));
group.add(new Statistic("Worst", min_economy, m_calcEngine.getEconomyUnits()));
group.add(new Statistic(getString(R.string.last), fillups.get(fillups.size() - 1).calcEconomy(), m_calcEngine.getEconomyUnits()));
return group;
}
public StatisticsGroup calcPrices(final List<FillUp> fillups) {
StatisticsGroup group = new StatisticsGroup(getString(R.string.fuel_prices));
double min_price = Double.MAX_VALUE;
double max_price = 0D;
double total_price = 0D;
for (FillUp fillup : fillups) {
double price = fillup.getPrice();
total_price += price;
if (price < min_price) {
min_price = price;
}
if (price > max_price) {
max_price = price;
}
}
group.add(new Statistic(getString(R.string.average), m_preferences.getCurrency(), total_price / fillups.size(), "/" + m_calcEngine.getVolumeUnitsAbbr()));
group.add(new Statistic(getString(R.string.maximum), m_preferences.getCurrency(), max_price, "/" + m_calcEngine.getVolumeUnitsAbbr()));
group.add(new Statistic(getString(R.string.minimum), m_preferences.getCurrency(), min_price, "/" + m_calcEngine.getVolumeUnitsAbbr()));
group.add(new Statistic(getString(R.string.last), m_preferences.getCurrency(), fillups.get(fillups.size() - 1).getPrice(), "/" + m_calcEngine.getVolumeUnitsAbbr()));
return group;
}
public StatisticsGroup calcCosts(final List<FillUp> fillups) {
StatisticsGroup group = new StatisticsGroup(getString(R.string.fillup_costs));
double min_cost = Double.MAX_VALUE;
double max_cost = 0D;
double total_cost = 0D;
double min_cost_per_mile = Double.MAX_VALUE;
double max_cost_per_mile = 0D;
double avg_cost_per_mile = 0D;
int count = 0;
for (FillUp fillup : fillups) {
double cost = fillup.calcCost();
total_cost += cost;
if (cost < min_cost) {
min_cost = cost;
}
if (cost > max_cost) {
max_cost = cost;
}
double cost_per_mile = fillup.calcCostPerDistance();
if (cost_per_mile < 0) {
continue;
}
if (cost_per_mile < min_cost_per_mile) {
min_cost_per_mile = cost_per_mile;
}
if (cost_per_mile > max_cost_per_mile) {
max_cost_per_mile = cost_per_mile;
}
avg_cost_per_mile += cost_per_mile;
count++;
}
FillUp last = fillups.get(fillups.size() - 1);
double last_cost = last.calcCost();
double last_cost_per_mile = last.calcCostPerDistance();
double avg_cost = total_cost / fillups.size();
avg_cost_per_mile = avg_cost_per_mile / ((double) count);
String distanceUnits = m_calcEngine.getDistanceUnitsAbbr().trim();
group.add(new Statistic(getString(R.string.average), m_preferences.getCurrency(), avg_cost));
group.add(new Statistic(getString(R.string.maximum), m_preferences.getCurrency(), max_cost));
group.add(new Statistic(getString(R.string.minimum), m_preferences.getCurrency(), min_cost));
group.add(new Statistic(getString(R.string.last), m_preferences.getCurrency(), last_cost));
DecimalFormat fmt = new DecimalFormat("0.000");
group.add(new Statistic(String.format(getString(R.string.average_cost_per), distanceUnits), m_preferences.getCurrency(), avg_cost_per_mile, fmt));
group.add(new Statistic(String.format(getString(R.string.maximum_cost_per), distanceUnits), m_preferences.getCurrency(), max_cost_per_mile, fmt));
group.add(new Statistic(String.format(getString(R.string.minimum_cost_per), distanceUnits), m_preferences.getCurrency(), min_cost_per_mile, fmt));
group.add(new Statistic(String.format(getString(R.string.last_cost_per), distanceUnits), m_preferences.getCurrency(), last_cost_per_mile, fmt));
return group;
}
public StatisticsGroup calcAmounts(final List<FillUp> fillups) {
StatisticsGroup group = new StatisticsGroup(getString(R.string.fuel_amounts));
double min_amount = Double.MAX_VALUE;
double max_amount = 0D;
double total_amount = 0D;
for (FillUp fillup : fillups) {
double amount = fillup.getAmount();
total_amount += amount;
if (amount < min_amount) {
min_amount = amount;
}
if (amount > max_amount) {
max_amount = amount;
}
}
int ten_thousand_miles = (int) Math.ceil(m_calcEngine.convertDistance(PreferencesProvider.MILES, m_calcEngine.getOutputDistance(), 10000));
double distance = fillups.get(fillups.size() - 1).getOdometer() - fillups.get(0).getOdometer();
double fuel_per_10k = (m_calcEngine.convertVolume(total_amount) / m_calcEngine.convertDistance(distance)) * ten_thousand_miles;
group.add(new Statistic(getString(R.string.total), total_amount, m_calcEngine.getVolumeUnitsAbbr()));
group.add(new Statistic(getString(R.string.average), total_amount / fillups.size(), m_calcEngine.getVolumeUnitsAbbr()));
group.add(new Statistic(getString(R.string.maximum), max_amount, m_calcEngine.getVolumeUnitsAbbr()));
group.add(new Statistic(getString(R.string.minimum), min_amount, m_calcEngine.getVolumeUnitsAbbr()));
group.add(new Statistic(getString(R.string.last), fillups.get(fillups.size() - 1).getAmount(), m_calcEngine.getVolumeUnitsAbbr()));
group.add(new Statistic(String.format(getString(R.string.fuel_per), ten_thousand_miles, m_calcEngine.getDistanceUnitsAbbr()), fuel_per_10k, m_calcEngine.getVolumeUnitsAbbr()));
return group;
}
public StatisticsGroup calcExpenses(final List<FillUp> fillups) {
StatisticsGroup group = new StatisticsGroup(getString(R.string.fuel_expenses));
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(System.currentTimeMillis());
long thirty_days_ago = new GregorianCalendar(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) - 1, cal.get(Calendar.DAY_OF_MONTH)).getTimeInMillis();
long one_year_ago = new GregorianCalendar(cal.get(Calendar.YEAR) - 1, cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)).getTimeInMillis();
double thirty_day_total = 0D;
double yearly_total = 0D;
double total = 0D;
for (FillUp fillup : fillups) {
total += fillup.calcCost();
long time = fillup.getDate().getTimeInMillis();
if (time > thirty_days_ago) {
thirty_day_total += fillup.calcCost();
}
if (time > one_year_ago) {
yearly_total += fillup.calcCost();
}
}
group.add(new Statistic(getString(R.string.total), m_preferences.getCurrency(), total));
group.add(new Statistic(getString(R.string.last_month), m_preferences.getCurrency(), thirty_day_total));
group.add(new Statistic(getString(R.string.last_year), m_preferences.getCurrency(), yearly_total));
return group;
}
public StatisticsGroup calcLocations(final List<FillUp> fillups) {
StatisticsGroup group = new StatisticsGroup(getString(R.string.fillup_locations));
if (m_preferences.getBoolean(PreferencesProvider.LOCATION, true)) {
double north = -90;
double south = 90;
double east = -180;
double west = 180;
double lat, lon;
for (FillUp f : fillups) {
lat = f.getLatitude();
lon = f.getLongitude();
if (lat >= north) {
north = lat;
}
if (lat <= south) {
south = lat;
}
if (lon >= east) {
east = lon;
}
if (lon <= west) {
west = lon;
}
}
final String DEG = "°";
final DecimalFormat fmt = new DecimalFormat("0.0000");
group.add(new Statistic(getString(R.string.location_north), north, DEG, fmt));
group.add(new Statistic(getString(R.string.location_south), south, DEG, fmt));
group.add(new Statistic(getString(R.string.location_east), east, DEG, fmt));
group.add(new Statistic(getString(R.string.location_west), west, DEG, fmt));
}
return group;
}
private static class CalculationTask extends AsyncTask<Long, StatisticsGroup, Boolean> {
public StatisticsView activity;
private int t = 0;
private int p = 0;
private LinearLayout getContainer() {
return (LinearLayout) activity.findViewById(R.id.stats_container);
}
@Override
protected void onPreExecute() {
activity.showDialog(DIALOG_STATS_PROGRESS);
}
@Override
protected Boolean doInBackground(Long... ids) {
// TODO: optimize this. This has huge overhead
List<FillUp> fillups = new ArrayList<FillUp>();
String[] projection = FillUp.getProjection();
String selection = FillUp.VEHICLE_ID + " = ?";
String[] selectionArgs = new String[] {
String.valueOf(ids[0])
};
Cursor c = activity.getContentResolver().query(FillUp.CONTENT_URI, projection, selection, selectionArgs, FillUp.ODOMETER + " ASC");
c.moveToFirst();
t = c.getCount();
publishProgress();
FillUp prev = null;
FillUp curr = null;
while (c.isAfterLast() == false) {
curr = new FillUp(activity.m_calcEngine, c);
fillups.add(curr);
curr.setPrevious(prev);
if (prev != null) {
prev.setNext(curr);
}
prev = curr;
c.moveToNext();
p++;
publishProgress();
}
c.close();
if (fillups.size() >= 2) {
// crunch the numbers
// TODO: Is there a way to remove this copy-pasta?
if (!isCancelled())
publishProgress(activity.calcDistances(fillups));
if (!isCancelled())
publishProgress(activity.calcEconomy(fillups));
if (!isCancelled())
publishProgress(activity.calcPrices(fillups));
if (!isCancelled())
publishProgress(activity.calcCosts(fillups));
if (!isCancelled())
publishProgress(activity.calcAmounts(fillups));
if (!isCancelled())
publishProgress(activity.calcExpenses(fillups));
if (!isCancelled())
publishProgress(activity.calcLocations(fillups));
}
return true;
}
@Override
protected void onProgressUpdate(StatisticsGroup... update) {
if (!isCancelled()) {
if (update.length == 0) {
if (activity != null && activity.m_dlg != null) {
activity.m_dlg.setMax(t);
activity.m_dlg.setProgress(p);
}
} else {
getContainer().addView(update[0].render(activity));
}
}
}
@Override
protected void onPostExecute(Boolean result) {
activity.removeDialog(DIALOG_STATS_PROGRESS);
}
}
}