org.starfishrespect.myconsumption.android.ui.ChartViewFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.starfishrespect.myconsumption.android.ui.ChartViewFragment.java

Source

package org.starfishrespect.myconsumption.android.ui;

import android.app.Activity;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;

import org.starfishrespect.myconsumption.android.R;
import org.starfishrespect.myconsumption.android.SingleInstance;
import org.starfishrespect.myconsumption.android.dao.SensorValuesDao;
import org.starfishrespect.myconsumption.android.data.FrequencyData;
import org.starfishrespect.myconsumption.android.data.SensorData;
import org.starfishrespect.myconsumption.android.data.SensorValue;
import org.starfishrespect.myconsumption.android.data.SensorValuePreProcessor;
import org.achartengine.ChartFactory;
import org.achartengine.GraphicalView;
import org.achartengine.chart.PointStyle;
import org.achartengine.model.SeriesSelection;
import org.achartengine.model.XYMultipleSeriesDataset;
import org.achartengine.model.XYSeries;
import org.achartengine.renderer.XYMultipleSeriesRenderer;
import org.achartengine.renderer.XYSeriesRenderer;
import org.achartengine.util.IndexXYMap;
import org.starfishrespect.myconsumption.android.events.ColorChangedEvent;
import org.starfishrespect.myconsumption.android.events.DateChangedEvent;
import org.starfishrespect.myconsumption.android.events.FragmentsReadyEvent;
import org.starfishrespect.myconsumption.android.events.ReloadUserEvent;
import org.starfishrespect.myconsumption.android.events.UpdateMovingAverageEvent;
import org.starfishrespect.myconsumption.android.events.VisibilityChangedEvent;

import java.sql.SQLException;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.SimpleDateFormat;
import java.util.*;

import de.greenrobot.event.EventBus;

/**
 * Fragment that displays the chart of ChartActivity
 * S23Y (2015). Licensed under the Apache License, Version 2.0.
 * Adapted from Patrick by Thibaud Ledent
 */
public class ChartViewFragment extends Fragment {

    protected ChartActivity mActivity;

    private static final String TAG = "ChartViewFragment";

    private GraphicalView chart = null;

    /* ------------------------------------------
     * currentChartDataset & originalChartDataset
     * ------------------------------------------
     * Why keeping both of them? Because of the smoothing option given to the user, we need
     * to keep the original dataset in order to recompute the moving average if the slider is
     * modified. The currentChartDataset contains the values modified while the originalDataSet
     * contains the one that aren't.
     */
    // Dataset containing the series of point that is really displayed
    private XYMultipleSeriesDataset currentChartDataset = new XYMultipleSeriesDataset();
    // Dataset containing the series of point that is present in the db
    private XYMultipleSeriesDataset originalChartDataset = new XYMultipleSeriesDataset();

    private XYMultipleSeriesRenderer chartRenderer = new XYMultipleSeriesRenderer();
    private double minimalX = Integer.MAX_VALUE, maximalX = 0, maximalY = 0;
    private HashMap<String, ChartSerieRendererContainer> data;
    private float touchPosX = 0, touchPosY = 0;

    private int currentStart, currentDateDelay, currentValueDelay;

    private TextView textViewNoData, textViewDataSensorName, textViewDataDate, textViewDataValue;
    private View colorViewSelectedData;
    private RelativeLayout layoutPointData;
    private View refreshingView;
    private LinearLayout chartLayout;
    private boolean refreshing = false;
    private int seekBarPosition = 0;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_chart_view, container, false);

        // Register to the EventBus
        EventBus.getDefault().register(this);

        textViewNoData = (TextView) view.findViewById(R.id.textViewNoData);
        textViewDataSensorName = (TextView) view.findViewById(R.id.textViewDataSensorName);
        textViewDataDate = (TextView) view.findViewById(R.id.textViewDataDate);
        textViewDataValue = (TextView) view.findViewById(R.id.textViewDataValue);
        refreshingView = view.findViewById(R.id.layoutChartRefresh);
        layoutPointData = (RelativeLayout) view.findViewById(R.id.layoutPointData);
        colorViewSelectedData = view.findViewById(R.id.colorViewSelectedData);
        chartLayout = (LinearLayout) view.findViewById(R.id.chartContainer);

        return view;
    }

    @Override
    public void onStart() {
        super.onStart();
        EventBus.getDefault().post(new FragmentsReadyEvent(this.getClass(), true));
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        mActivity = (ChartActivity) activity;
    }

    @Override
    public void onDestroy() {
        // Unregister to the EventBus
        EventBus.getDefault().unregister(this);
        super.onDestroy();
    }

    /**
     * Triggered when the user wants to reload data.
     * @param event A ReloadUser event
     */
    public void onEvent(ReloadUserEvent event) {
        // Reset and init the graph
        reset();
    }

    /**
     * Triggered when the date is changed in the spinner of ChartChoiceFragment.
     * @param event A ReloadUser event
     */
    public void onEvent(DateChangedEvent event) {
        if (event.getDate() == null) {
            return;
        }
        long date = event.getDate().getTime() / 1000;
        showAllGraphicsWithPrecision((int) date, event.getDateDelay(), event.getValueDelay());
    }

    /**
     * Triggered when the visibility is changed in ChartChoiceFragment.
     * @param event A VisibilityChangedEvent event
     */
    public void onEvent(VisibilityChangedEvent event) {
        setSensorVisibility(event.getSensor(), event.getSensor().isVisible());
    }

    /**
     * Triggered when the color is changed in ChartChoiceFragment.
     * @param event A ColorChangedEvent event
     */
    public void onEvent(ColorChangedEvent event) {
        setSensorColor(event.getSensor(), event.getSensor().getColor());
    }

    /**
     * Triggered when the moving average seekbar is changed in ChartChoiceFragment.
     * @param event A UpdateMovingAverageEvent event
     */
    public void onEvent(UpdateMovingAverageEvent event) {
        seekBarPosition = event.getSeekBarPosition();
        updateMovingAverage();
    }

    // load data from the local database to a sensor
    private List<SensorValue> loadData(SensorData sensor) {
        Log.d(TAG, "loading data");
        List<SensorValue> values;
        if (currentDateDelay == -1)
            values = new SensorValuesDao(SingleInstance.getDatabaseHelper()).getValues(sensor.getSensorId(),
                    currentStart, Integer.MAX_VALUE);
        else
            values = new SensorValuesDao(SingleInstance.getDatabaseHelper()).getValues(sensor.getSensorId(),
                    currentStart, currentStart + currentDateDelay);

        Log.d(TAG, "processing data");
        values = SensorValuePreProcessor.fitToPrecision(values, currentValueDelay);
        return values;
    }

    // refresh the chart for the given sensor
    private void updateChartForSensor(ChartSerieRendererContainer container) {
        if (container == null)
            return;

        XYSeriesRenderer serieRenderer = null;

        // Remove former serie form dataset and renderer if present
        if (container.getSerie() != null) {
            currentChartDataset.removeSeries(container.getSerie());
            originalChartDataset.removeSeries(container.getSerie());
        }
        if (container.getRenderer() != null) {
            chartRenderer.removeSeriesRenderer(container.getRenderer());
            serieRenderer = container.getRenderer();
        }

        // If the sensor is visible and the container has values
        if (container.getSensor().isVisible() && container.values != null && container.values.size() > 0) {
            if (serieRenderer == null) {
                serieRenderer = new XYSeriesRenderer();
                serieRenderer.setPointStyle(PointStyle.CIRCLE);
            }
            serieRenderer.setColor(container.getSensor().getColor());
            final XYSeries serie = new XYSeries(container.getSensor().getSensorId());
            for (SensorValue point : container.values) {
                int x = point.getTimestamp();
                int y = point.getValue();
                if (y > maximalY)
                    maximalY = y;
                if (x > maximalX)
                    maximalX = x;
                if (x < minimalX)
                    minimalX = x;
                serie.add(x, y);
            }

            double lateralDelta = maximalX - minimalX;
            lateralDelta /= 20;
            double[] limits = { minimalX - lateralDelta, maximalX + lateralDelta, 0.0, maximalY };
            chartRenderer.setPanLimits(limits);

            // Add series to the datasets
            originalChartDataset.addSeries(serie);
            currentChartDataset.addSeries(movingAverage(serie, seekBarPosition));

            // Add series to the renderer
            chartRenderer.addSeriesRenderer(serieRenderer);

            container.setRenderer(serieRenderer);
            container.setSerie(serie);

            if (chart == null) {
                chart = ChartFactory.getLineChartView(mActivity, currentChartDataset, chartRenderer);
                chartLayout.addView(chart, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
                        LinearLayout.LayoutParams.MATCH_PARENT));
                chart.setClickable(true);

                chart.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        SeriesSelection seriesSelection = chart.getCurrentSeriesAndPoint();

                        if (seriesSelection != null) {
                            for (ChartSerieRendererContainer container : data.values()) {
                                if (container.getSerie() == null) {
                                    continue;
                                }
                                if (container.values == null || container.values.size() == 0) {
                                    continue;
                                }

                                XYSeries xyserie = currentChartDataset
                                        .getSeriesAt(seriesSelection.getSeriesIndex());

                                if (container.getSensor().getSensorId().equals(xyserie.getTitle())) {
                                    int point = seriesSelection.getPointIndex();
                                    double px = seriesSelection.getXValue();
                                    long ts = (long) px;
                                    int nearest = -1;
                                    for (int i = 0; i < container.values.size(); i++) {
                                        if (ts - container.values.get(i).getTimestamp() >= 0) {
                                            nearest = i;
                                        } else {
                                            point = nearest;
                                            break;
                                        }
                                    }
                                    // correcting point, if some values on the left are not visible

                                    layoutPointData.setVisibility(View.INVISIBLE);
                                    textViewDataSensorName.setText(container.sensor.getName());
                                    textViewDataDate.setText(formatDate(
                                            new Date(((long) container.values.get(point).getTimestamp()) * 1000)));
                                    textViewDataValue.setText(container.values.get(point).getValue() + " W");
                                    colorViewSelectedData.setBackgroundColor(container.getSensor().getColor());
                                    float posX = touchPosX;
                                    float posY = touchPosY;
                                    if (posX > v.getWidth() / 2) {
                                        posX = posX - layoutPointData.getWidth() - 10;
                                    } else {
                                        posX++;
                                    }
                                    if (posY + layoutPointData.getHeight() + 10 > v.getHeight()) {
                                        posY = posY - layoutPointData.getHeight() - 10;
                                    } else {
                                        posY += 10;
                                    }
                                    layoutPointData.setX(posX);
                                    layoutPointData.setY(posY);
                                    layoutPointData.setVisibility(View.VISIBLE);
                                    break;
                                }
                            }
                        } else {
                            layoutPointData.setVisibility(View.INVISIBLE);
                        }

                    }
                });

                chart.setOnTouchListener(new View.OnTouchListener() {
                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        if (event.getAction() == MotionEvent.ACTION_UP) {
                            touchPosX = event.getX();
                            touchPosY = event.getY();
                        }
                        return false;
                    }
                });
            }
        }
        if (chart != null) {
            chart.repaint();
        }
        if (currentChartDataset.getSeriesCount() == 0) {
            textViewNoData.setVisibility(View.VISIBLE);
        } else {
            textViewNoData.setVisibility(View.GONE);
        }
    }

    /**
     * Formats the date to a pretty display for information popup,
     * adapted with the current precision
     *
     * @param date the date
     * @return the formatted date
     */
    private String formatDate(Date date) {
        String format = "";
        switch (currentValueDelay) {
        case FrequencyData.DELAY_DAY:
            format = "EEE dd MMMM yyyy";
            break;
        case FrequencyData.DELAY_15MIN:
        case FrequencyData.DELAY_MINUTE:
        case FrequencyData.DELAY_FIVE_MINUTES:
        case FrequencyData.DELAY_HOUR:
            format = "EEE dd MMMM yyyy HH:mm";
            break;
        case FrequencyData.DELAY_MONTH:
            format = "MMMM yyyy";
            break;
        case FrequencyData.DELAY_YEAR:
            format = "yyyy";
            break;
        case FrequencyData.DELAY_WEEK:
            Date nextWeek = new Date(date.getTime() + 6000 * 86400);
            format = "EEE dd MMMM yyyy";
            SimpleDateFormat formatter = new SimpleDateFormat(format);
            return "Week from " + formatter.format(date) + " to " + formatter.format(nextWeek);

        }
        return new SimpleDateFormat(format).format(date);
    }

    /**
     * Show all data that is marked as visible on the graph with the given interval
     *
     * @param start      start of the range to display (timestamp)
     * @param dateDelay  length of the range to display
     * @param valueDelay minimal time between two successive points. If time if longer,
     *                   multiple points will be aggregated
     */
    public void showAllGraphicsWithPrecision(int start, int dateDelay, int valueDelay) {
        if (refreshing) {
            return;
        }

        if (start == currentStart && dateDelay == currentDateDelay && valueDelay == currentValueDelay
                && dateDelay != -1) {
            return;
        }
        try {
            layoutPointData.setVisibility(View.GONE);
        } catch (NullPointerException e) {
            return;
        }
        init();
        currentStart = start;
        currentDateDelay = dateDelay;
        currentValueDelay = valueDelay;
        refreshingView.setVisibility(View.VISIBLE);
        textViewNoData.setVisibility(View.GONE);
        new loadLocalDataTask().execute();
    }

    // clears the graph
    public void reset() {
        Log.d(TAG, "reset");
        currentStart = -1;
        currentDateDelay = -1;
        currentValueDelay = -1;
        init();
    }

    // initialises the graph
    private void init() {
        Log.d(TAG, "init");
        layoutPointData.setVisibility(View.GONE);
        chartLayout.removeAllViews();
        chart = null;
        data = new HashMap<>();
        List<SensorData> sensors = SingleInstance.getUserController().getUser().getSensors();
        if (sensors.size() == 0) {
            textViewNoData.setVisibility(View.VISIBLE);
            textViewNoData.setText(R.string.chart_text_no_sensor);
            refreshingView.setVisibility(View.GONE);
        } else {
            textViewNoData.setText(R.string.chart_text_no_data);
        }
        for (SensorData sensor : sensors) {
            data.put(sensor.getSensorId(), new ChartSerieRendererContainer(sensor));
        }
        textViewNoData.setVisibility(View.VISIBLE);

        originalChartDataset = new XYMultipleSeriesDataset();
        currentChartDataset = new XYMultipleSeriesDataset();
        chartRenderer = new XYMultipleSeriesRenderer();

        chartRenderer.setApplyBackgroundColor(true);
        chartRenderer.setBackgroundColor(Color.WHITE);
        chartRenderer.setAxesColor(Color.DKGRAY);
        chartRenderer.setMarginsColor(Color.WHITE);
        chartRenderer.setGridColor(Color.LTGRAY);
        chartRenderer.setXLabelsColor(Color.DKGRAY);
        chartRenderer.setYLabelsColor(0, Color.DKGRAY);

        //chartRenderer.setZoomEnabled(true);
        //chartRenderer.setPanEnabled(true);
        chartRenderer.setZoomEnabled(true, false);
        chartRenderer.setPanEnabled(true, false);
        chartRenderer.setClickEnabled(true);

        chartRenderer.setShowGrid(true);
        chartRenderer.setXLabelFormat(new DecimalFormat() {
            @Override
            public StringBuffer format(double value, StringBuffer buffer, FieldPosition position) {
                Date dateFormat = new Date(((long) value) * 1000);
                buffer.append(new SimpleDateFormat("yyyy-MM-dd HH:mm").format(dateFormat));
                return buffer;
            }
        });

        chartRenderer.setShowLegend(false);

        minimalX = Integer.MAX_VALUE;
        maximalX = 0;
        maximalY = 0;
    }

    /**
     * Apply a moving average on the dataset based on the smoothing option chosen by the user.
     * It updates the current data set displayed on screen based on the values given in the
     * original data set.
     */
    public void updateMovingAverage() {
        if (seekBarPosition < 0)
            return;

        currentChartDataset.clear();

        // Take into account each sensor and its associated series.
        for (XYSeries series : originalChartDataset.getSeries()) {
            XYSeries moving = movingAverage(series, seekBarPosition);

            currentChartDataset.addSeries(moving);
        }

        if (chart != null) {
            chart.repaint();
        } else
            Log.d(TAG, "in method updateMovingAverage: chart is null while it shouldn't");
    }

    // Computes the moving average from a given series
    private XYSeries movingAverage(XYSeries series, int N) {
        if (series == null | N < 0)
            return null;

        if (N == 0)
            return series;

        Log.d(TAG, "method movingAverage called; slider position = " + N);

        XYSeries moving = new XYSeries(series.getTitle());
        IndexXYMap<Double, Double> iXY = (IndexXYMap<Double, Double>) series.getXYMap().clone();

        Double y_n_1 = 0.0;

        for (int i = 1; i < iXY.size(); i++) {
            Double av;

            if ((i - N) < 0)
                continue;
            else
                av = y_n_1 + ((iXY.getYByIndex(i) - iXY.getYByIndex(i - N)) / N);

            moving.add(iXY.getXByIndex(i), av);
            y_n_1 = av;
        }

        return moving;
    }

    // simple container for data series
    private class ChartSerieRendererContainer {
        XYSeries serie;
        XYSeriesRenderer renderer;
        SensorData sensor;
        List<SensorValue> values;

        private ChartSerieRendererContainer(SensorData sensor) {
            this.sensor = sensor;
        }

        public void setSerie(XYSeries serie) {
            this.serie = serie;
        }

        public void setRenderer(XYSeriesRenderer renderer) {
            this.renderer = renderer;
        }

        public XYSeries getSerie() {
            return serie;
        }

        public XYSeriesRenderer getRenderer() {
            return renderer;
        }

        public SensorData getSensor() {
            return sensor;
        }

        public void setSensor(SensorData sensor) {
            this.sensor = sensor;
        }
    }

    // shows or hide a sensor from the graph
    public void setSensorVisibility(SensorData sensor, boolean visible) {
        layoutPointData.setVisibility(View.GONE);
        ChartSerieRendererContainer container = data.get(sensor.getSensorId());
        if (container == null) {
            return;
        }
        container.getSensor().setVisible(visible);
        try {
            SingleInstance.getDatabaseHelper().getSensorDao().update(container.getSensor());
        } catch (SQLException e) {
            e.printStackTrace();
        }
        new loadLocalDataTask().execute(sensor);
    }

    // change the color of a sensor of the graph
    public void setSensorColor(SensorData sensor, int color) {
        layoutPointData.setVisibility(View.GONE);
        ChartSerieRendererContainer container = data.get(sensor.getSensorId());
        if (container == null) {
            return;
        }
        container.getSensor().setColor(color);
        try {
            SingleInstance.getDatabaseHelper().getSensorDao().update(container.getSensor());
        } catch (SQLException e) {
            e.printStackTrace();
        }
        new loadLocalDataTask().execute(sensor);
    }

    // task to load a data serie from the database, to avoid blocking
    private class loadLocalDataTask extends AsyncTask<SensorData, ChartSerieRendererContainer, Void> {
        @Override
        protected void onPreExecute() {
            refreshing = true;
        }

        @Override
        protected Void doInBackground(SensorData... sensors) {

            if (sensors.length == 0) {
                for (ChartSerieRendererContainer container : data.values()) {
                    if (!container.getSensor().isVisible()) {
                        Log.d(TAG, "INVISIBLE");
                        publishProgress(container);
                        continue;
                    }
                    if (container.values == null) {
                        container.values = loadData(container.sensor);
                    }
                    publishProgress(container);
                }
            } else {
                for (SensorData sensor : sensors) {
                    ChartSerieRendererContainer container = data.get(sensor.getSensorId());
                    if (container == null) {
                        continue;
                    }
                    if (!container.getSensor().isVisible()) {
                        Log.d(TAG, "INVISIBLE");
                        publishProgress(container);
                        continue;
                    }
                    if (container.values == null) {
                        container.values = loadData(container.sensor);
                    }
                    publishProgress(container);
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(ChartSerieRendererContainer... values) {
            for (ChartSerieRendererContainer container : values) {
                updateChartForSensor(container);
            }
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            refreshingView.setVisibility(View.GONE);
            refreshing = false;
        }
    }

}