org.cowboycoders.cyclismo.fragments.AltitudeProfileFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.cowboycoders.cyclismo.fragments.AltitudeProfileFragment.java

Source

/*
*    Copyright (c) 2013, Will Szumski
*    Copyright (c) 2013, Doug Szumski
*
*    This file is part of Cyclismo.
*
*    Cyclismo 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.
*
*    Cyclismo 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 Cyclismo.  If not, see <http://www.gnu.org/licenses/>.
*/
/*
 * Copyright 2009 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package org.cowboycoders.cyclismo.fragments;

import android.location.Location;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ZoomControls;

import com.google.common.annotations.VisibleForTesting;

import org.cowboycoders.cyclismo.AltitudeProfileView;
import org.cowboycoders.cyclismo.R;
import org.cowboycoders.cyclismo.TrackDetailActivity;
import org.cowboycoders.cyclismo.content.Track;
import org.cowboycoders.cyclismo.content.TrackDataHub;
import org.cowboycoders.cyclismo.content.TrackDataListener;
import org.cowboycoders.cyclismo.content.TrackDataType;
import org.cowboycoders.cyclismo.content.Waypoint;
import org.cowboycoders.cyclismo.stats.TripStatistics;
import org.cowboycoders.cyclismo.stats.TripStatisticsUpdater;
import org.cowboycoders.cyclismo.util.LocationUtils;
import org.cowboycoders.cyclismo.util.PreferencesUtils;
import org.cowboycoders.cyclismo.util.UnitConversions;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * A fragment to display track chart to the user.
 *
 * @author Sandor Dornbush
 * @author Rodrigo Damazio
 */
public class AltitudeProfileFragment extends Fragment implements TrackDataListener {

    public static final String TAG = "AltitudeProfileFragment";
    public static final String CHART_FRAGMENT_TAG = "altitudeProfileFragment";

    private final ArrayList<double[]> pendingPoints = new ArrayList<double[]>();
    private final ArrayList<double[]> pendingOverlayPoints = new ArrayList<double[]>();

    private TrackDataHub trackDataHub;

    private Track currentTrack;

    private Lock classLock = new ReentrantLock();

    private Condition startCondition = classLock.newCondition();

    // Stats gathered from the received datCa
    private TripStatisticsUpdater tripStatisticsUpdater;
    private TripStatisticsUpdater tripStatisticsUpdaterOverlay;
    private long startTime = -1L;

    private boolean metricUnits = PreferencesUtils.METRIC_UNITS_DEFAULT;
    private int minRecordingDistance = PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT;

    // Modes of operation
    private boolean chartByDistance = true;
    private boolean[] chartShow = new boolean[] { true, true, true, true, true, true };

    long currentCourseId = -1L;

    // UI elements
    private AltitudeProfileView altitudeProfileView;
    private ZoomControls zoomControls;

    private boolean overlayCourseData = false;

    /**
     * A runnable that will enable/disable zoom controls and orange pointer as
     * appropriate and redraw.
     */
    private final Runnable updateChart = new Runnable() {
        @Override
        public void run() {
            if (!isResumed() || trackDataHub == null) {
                return;
            }

            zoomControls.setIsZoomInEnabled(altitudeProfileView.canZoomIn());
            zoomControls.setIsZoomOutEnabled(altitudeProfileView.canZoomOut());
            altitudeProfileView.invalidate();
        }
    };

    private TrackDataHub courseDataHub;

    // would need to zoom in to relevant section to use
    // (as is stretches scale too much and zoom limit prevents from seeing new data)
    // also x-axis should be restricted to distance
    private TrackDataListener courseTrackDataListener = new TrackDataListener() {

        private Track currentCourse;
        private boolean overlayAdded = false;

        @Override
        public void onLocationStateChanged(LocationState state) {
            // We don't care.
        }

        @Override
        public void onLocationChanged(Location loc) {
            // We don't care.
        }

        @Override
        public void onHeadingChanged(double heading) {
            // We don't care.
        }

        @Override
        public void onSelectedTrackChanged(Track track) {
            // We don't care.
        }

        @Override
        public void onTrackUpdated(Track track) {
            if (isResumed()) {
                Log.d(TAG, "course updated");
                currentCourse = track;
            }
        }

        @Override
        public synchronized void clearTrackPoints() {
            if (isResumed()) {
                //      Log.d(TAG,"track points cleared");

                try {
                    // FIXME: appears to be waiting for startTime, but could have our own startTime, as
                    // in track loader
                    while (startTime == -1l) {
                        try {
                            classLock.lock();
                            startCondition.await();
                        } finally {
                            classLock.unlock();
                        }
                    }

                } catch (InterruptedException e) {
                    // use start time as is
                }

                tripStatisticsUpdaterOverlay = startTime != -1L ? new TripStatisticsUpdater(startTime) : null;
                //      pendingPoints.clear();
                //      altitudeProfileView.reset();
                //      getActivity().runOnUiThread(new Runnable() {
                //          @Override
                //        public void run() {
                //          if (isResumed()) {
                //            altitudeProfileView.resetScroll();
                //          }
                //        }
                //      });
            }
        }

        @Override
        public synchronized void onSampledInTrackPoint(Location location) {
            if (isResumed()) {
                Log.d(TAG, "adding course point");
                double[] data = new double[AltitudeProfileView.NUM_SERIES + 1];
                fillDataPoint(location, data, tripStatisticsUpdaterOverlay);
                pendingOverlayPoints.add(data);
            }
        }

        @Override
        public void onSampledOutTrackPoint(Location location) {

        }

        @Override
        public void onSegmentSplit(Location location) {
            if (isResumed()) {
                fillDataPoint(location, null, tripStatisticsUpdaterOverlay);
            }
        }

        @Override
        public synchronized void onNewTrackPointsDone() {
            if (isResumed()) {
                Log.d(TAG, "course points done");
                if (overlayCourseData && currentCourse != null && currentCourse.getId() == currentCourseId
                        && !overlayAdded) {
                    altitudeProfileView.addAltitudeData(pendingOverlayPoints);
                    getActivity().runOnUiThread(updateChart);
                    overlayAdded = true;
                }
                pendingOverlayPoints.clear();
            }
        }

        @Override
        public void clearWaypoints() {

        }

        @Override
        public void onNewWaypoint(Waypoint waypoint) {
            //    if (isResumed() && waypoint != null && LocationUtils.isValidLocation(waypoint.getLocation())) {
            //      altitudeProfileView.addWaypoint(waypoint);
            //    }
        }

        @Override
        public void onNewWaypointsDone() {
            //    if (isResumed()) {
            //      getActivity().runOnUiThread(updateChart);
            //    }
        }

        @Override
        public boolean onMetricUnitsChanged(boolean metric) {
            return false;
        }

        @Override
        public boolean onReportSpeedChanged(boolean speed) {
            return false;

        }

        @Override
        public boolean onMinRecordingDistanceChanged(int value) {
            return false;
        }
    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        /*
         * Create a altitudeProfileView here to store data thus won't need to reload all the
         * data on every onStart or onResume.
         */
        altitudeProfileView = new AltitudeProfileView(getActivity());
    }

    ;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.altitude_profile, container, false);
        zoomControls = (ZoomControls) view.findViewById(R.id.altitude_profile_zoom_controls);
        zoomControls.setOnZoomInClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                zoomIn();
            }
        });
        zoomControls.setOnZoomOutClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                zoomOut();
            }
        });
        return view;
    }

    @Override
    public void onStart() {
        super.onStart();
        ViewGroup layout = (ViewGroup) getActivity().findViewById(R.id.altitude_profile_layout);
        LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        layout.addView(altitudeProfileView, layoutParams);
    }

    @Override
    public void onResume() {
        super.onResume();
        //TODO: why only in course mode? We need it to work in existing rides.
        //if (((TrackDetailActivity) getActivity()).isCourseMode()) {
        overlayCourseData = true;

        //}
        resumeTrackDataHub();
        resumeCourseDataHub();

        checkChartSettings();
        getActivity().runOnUiThread(updateChart);

        // show elevation for whole course if in course mode
        if (overlayCourseData) {
            altitudeProfileView.setOverlayChartValueSeriesEnabled(AltitudeProfileView.ELEVATION_SERIES, true);
        } else {
            altitudeProfileView.setOverlayChartValueSeriesEnabled(AltitudeProfileView.ELEVATION_SERIES, false);
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        pauseTrackDataHub();
        pauseCourseDataHub();
    }

    @Override
    public void onStop() {
        super.onStop();
        ViewGroup layout = (ViewGroup) getActivity().findViewById(R.id.altitude_profile_layout);
        layout.removeView(altitudeProfileView);
    }

    @Override
    public void onLocationStateChanged(LocationState state) {
        // We don't care.
    }

    @Override
    public void onLocationChanged(Location loc) {
        // We don't care.
    }

    @Override
    public void onHeadingChanged(double heading) {
        // We don't care.
    }

    @Override
    public void onSelectedTrackChanged(Track track) {
        // We don't care.
    }

    @Override
    public void onTrackUpdated(Track track) {
        if (isResumed()) {
            Log.d(TAG, "track updated");
            currentTrack = track;
            if (track == null || track.getTripStatistics() == null) {
                startTime = -1L;
                return;
            }
            startTime = track.getTripStatistics().getStartTime();
            try {
                classLock.lock();
                startCondition.signalAll();
            } finally {
                classLock.unlock();
            }
        }
    }

    @Override
    public synchronized void clearTrackPoints() {
        if (isResumed()) {
            Log.d(TAG, "track points cleared");
            tripStatisticsUpdater = startTime != -1L ? new TripStatisticsUpdater(startTime) : null;
            pendingPoints.clear();
            altitudeProfileView.reset();
            getActivity().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (isResumed()) {
                        altitudeProfileView.resetScroll();
                    }
                }
            });
        }
    }

    @Override
    public synchronized void onSampledInTrackPoint(Location location) {
        if (isResumed()) {
            Log.d(TAG, "adding point");
            Log.d(TAG, "onSampliedInTrack elevation: " + location.getAltitude());
            double[] data = new double[AltitudeProfileView.NUM_SERIES + 1];

            fillDataPoint(location, data, tripStatisticsUpdater);
            pendingPoints.add(data);
        }
    }

    @Override
    public void onSampledOutTrackPoint(Location location) {
        if (isResumed()) {
            fillDataPoint(location, null, tripStatisticsUpdater);
        }
    }

    @Override
    public void onSegmentSplit(Location location) {
        if (isResumed()) {
            fillDataPoint(location, null, tripStatisticsUpdater);
        }
    }

    @Override
    public synchronized void onNewTrackPointsDone() {
        if (isResumed()) {
            Log.d(TAG, "track points done");
            altitudeProfileView.addDataPoints(pendingPoints);
            pendingPoints.clear();
            getActivity().runOnUiThread(updateChart);
        }
    }

    @Override
    public void clearWaypoints() {
        if (isResumed()) {
            altitudeProfileView.clearWaypoints();
        }
    }

    @Override
    public void onNewWaypoint(Waypoint waypoint) {
        if (isResumed() && waypoint != null && LocationUtils.isValidLocation(waypoint.getLocation())) {
            altitudeProfileView.addWaypoint(waypoint);
        }
    }

    @Override
    public void onNewWaypointsDone() {
        if (isResumed()) {
            getActivity().runOnUiThread(updateChart);
        }
    }

    @Override
    public boolean onMetricUnitsChanged(boolean metric) {
        if (isResumed()) {
            if (metricUnits == metric) {
                return false;
            }
            metricUnits = metric;
            altitudeProfileView.setMetricUnits(metricUnits);
            getActivity().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (isResumed()) {
                        altitudeProfileView.requestLayout();
                    }
                }
            });
            return true;
        }
        return false;
    }

    @Override
    public boolean onReportSpeedChanged(boolean speed) {
        return false;
    }

    @Override
    public boolean onMinRecordingDistanceChanged(int value) {
        if (isResumed()) {
            if (minRecordingDistance == value) {
                return false;
            }
            minRecordingDistance = value;
            return true;
        }
        return false;
    }

    /**
     * Checks the chart settings.
     */
    private void checkChartSettings() {
        boolean needUpdate = false;
        if (chartByDistance != PreferencesUtils.getBoolean(getActivity(), R.string.chart_by_distance_key,
                PreferencesUtils.CHART_BY_DISTANCE_DEFAULT)) {
            chartByDistance = !chartByDistance;
            altitudeProfileView.setChartByDistance(chartByDistance);
            reloadTrackDataHub();
            reloadCourseDataHub();
            needUpdate = true;
        }
        if (setSeriesEnabled(AltitudeProfileView.ELEVATION_SERIES, PreferencesUtils.getBoolean(getActivity(),
                R.string.chart_show_elevation_key, PreferencesUtils.CHART_SHOW_ELEVATION_DEFAULT))) {
            needUpdate = true;
        }

        if (needUpdate) {
            altitudeProfileView.postInvalidate();
        }
    }

    /**
     * Sets the series enabled value.
     *
     * @param index the series index
     * @param value the value
     * @return true if changed
     */
    private boolean setSeriesEnabled(int index, boolean value) {
        if (chartShow[index] != value) {
            chartShow[index] = value;
            altitudeProfileView.setChartValueSeriesEnabled(index, value);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Resumes the trackDataHub. Needs to be synchronized because trackDataHub can
     * be accessed by multiple threads.
     */
    private synchronized void resumeTrackDataHub() {
        trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
        trackDataHub.registerTrackDataListener(this,
                EnumSet.of(TrackDataType.TRACKS_TABLE, TrackDataType.WAYPOINTS_TABLE,
                        TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE,
                        TrackDataType.PREFERENCE));
    }

    /**
     * Pauses the trackDataHub. Needs to be synchronized because trackDataHub can
     * be accessed by multiple threads.
     */
    private synchronized void pauseTrackDataHub() {
        trackDataHub.unregisterTrackDataListener(this);
        trackDataHub = null;
    }

    /**
     * Returns true if the selected track is recording. Needs to be synchronized
     * because trackDataHub can be accessed by multiple threads.
     */
    private synchronized boolean isSelectedTrackRecording() {
        return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
    }

    /**
     * Reloads the trackDataHub. Needs to be synchronized because trackDataHub can
     * be accessed by multiple threads.
     */
    private synchronized void reloadTrackDataHub() {
        if (trackDataHub != null) {
            trackDataHub.reloadDataForListener(this);
        }
    }

    /**
     * Reloads the courseDataHub. Needs to be synchronized because trackDataHub can
     * be accessed by multiple threads.
     */
    private synchronized void reloadCourseDataHub() {
        if (courseDataHub != null) {
            courseDataHub.loadTrack(currentCourseId);
            courseDataHub.reloadDataForListener(courseTrackDataListener);
        }
    }

    /**
     * Pauses the trackDataHub. Needs to be synchronized because the trackDataHub
     * can be accessed by multiple threads.
     */
    private synchronized void pauseCourseDataHub() {
        if (courseDataHub != null) {
            courseDataHub.unregisterTrackDataListener(courseTrackDataListener);
        }
        courseDataHub = null;
    }

    /**
     * Resumes the trackDataHub. Needs to be synchronized because the trackDataHub
     * can be accessed by multiple threads.
     */
    private synchronized void resumeCourseDataHub() {
        if (overlayCourseData) {
            TrackDetailActivity activity = (TrackDetailActivity) getActivity();
            courseDataHub = activity.getCourseDataHub();

            if (activity.isCourseMode()) {
                currentCourseId = activity.getCourseTrackId();
            } else {
                currentCourseId = activity.getTrackId();
                courseDataHub = activity.getTrackDataHub();
            }

            courseDataHub.registerTrackDataListener(courseTrackDataListener,
                    EnumSet.of(TrackDataType.TRACKS_TABLE, TrackDataType.SELECTED_TRACK,
                            TrackDataType.WAYPOINTS_TABLE, TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
                            TrackDataType.LOCATION));
            reloadCourseDataHub();

        }

    }

    /**
     * To zoom in.
     */
    private void zoomIn() {
        altitudeProfileView.zoomIn();
        zoomControls.setIsZoomInEnabled(altitudeProfileView.canZoomIn());
        zoomControls.setIsZoomOutEnabled(altitudeProfileView.canZoomOut());
    }

    /**
     * To zoom out.
     */
    private void zoomOut() {
        altitudeProfileView.zoomOut();
        zoomControls.setIsZoomInEnabled(altitudeProfileView.canZoomIn());
        zoomControls.setIsZoomOutEnabled(altitudeProfileView.canZoomOut());
    }

    /**
     * Given a location, fill in a data point, an array of double[]. <br>
     * data[0] = time/distance <br>
     * data[1] = elevation <br>
     * data[2] = speed <br>
     * data[3] = pace <br>
     * data[4] = heart rate <br>
     * data[5] = cadence <br>
     * data[6] = power <br>
     *
     * @param location the location
     * @param data     the data point to fill in, can be null
     */
    @VisibleForTesting
    void fillDataPoint(Location location, double data[], TripStatisticsUpdater tripStatisticsUpdaterIn) {
        double timeOrDistance = Double.NaN;
        double elevation = Double.NaN;

        if (tripStatisticsUpdaterIn != null) {
            tripStatisticsUpdaterIn.addLocation(location, minRecordingDistance);
            TripStatistics tripStatistics = tripStatisticsUpdaterIn.getTripStatistics();
            if (chartByDistance) {
                double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
                if (!metricUnits) {
                    distance *= UnitConversions.KM_TO_MI;
                }
                timeOrDistance = distance;
            } else {
                timeOrDistance = tripStatistics.getTotalTime();
            }
            elevation = tripStatisticsUpdaterIn.getSmoothedElevation();
            if (!metricUnits) {
                elevation *= UnitConversions.M_TO_FT;
            }

            if (data != null) {
                data[0] = timeOrDistance;
                data[1] = elevation;
                Log.d(TAG, "filldataPoint: elevation: " + elevation);
            }
        }
    }

    /**
     * Non-overlay default {@link org.cowboycoders.cyclismo.fragments.AltitudeProfileFragment#fillDataPoint(android.location.Location, double[], org.cowboycoders.cyclismo.stats.TripStatisticsUpdater)}
     *
     * @param location
     * @param data
     */
    @VisibleForTesting
    void fillDataPoint(Location location, double data[]) {
        this.fillDataPoint(location, data, tripStatisticsUpdater);
    }

    @VisibleForTesting
    AltitudeProfileView getAltitudeProfileView() {
        return altitudeProfileView;
    }

    @VisibleForTesting
    void setTripStatisticsUpdater(long time) {
        tripStatisticsUpdater = new TripStatisticsUpdater(time);
    }

    @VisibleForTesting
    void setAltitudeProfileView(AltitudeProfileView view) {
        altitudeProfileView = view;
    }

    @VisibleForTesting
    void setMetricUnits(boolean value) {
        metricUnits = value;
    }

    @VisibleForTesting
    void setChartByDistance(boolean value) {
        chartByDistance = value;
    }

}