/* * 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 2010 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.stats; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; import org.cowboycoders.cyclismo.lib.CyclismoLibConstants; /** * Statistical data about a trip. The data in this class should be filled out by * TripStatisticsBuilder. * <p> * TODO: hashCode and equals * * @author Rodrigo Damazio */ public class TripStatistics implements Parcelable { public static final String TAG = "TripStatistics"; // The trip start time. This is the system time, might not match the GPs time. private long startTime = -1L; // The trip stop time. This is the system time, might not match the GPS time. private long stopTime = -1L; // The total trip distance (meters). private double totalDistance; // The total time (ms). Updated when new points are received, may be stale. private long totalTime; // The total moving time (ms). Based on when we believe the user is traveling. private long movingTime; // The min and max latitude seen in this trip. private final ExtremityMonitor latitudeExtremities = new ExtremityMonitor(); // The min and max longitude seen in this trip. private final ExtremityMonitor longitudeExtremities = new ExtremityMonitor(); // The maximum speed (meters/second) that we believe is valid. private double maxSpeed; // The min and max elevation (meters) seen on this trip. private final ExtremityMonitor elevationExtremities = new ExtremityMonitor(); // The total elevation gained (meters). private double totalElevationGain; // The min and max grade seen on this trip. private final ExtremityMonitor gradeExtremities = new ExtremityMonitor(); // Total useful (as in used to propel the bike) work done over the session duration (J). Used for // power calculations. private double totalWorkDone; // Total number of heart beats, used for calculating BPM. private double totalHeartBeats; // Total number of crank rotations, used for calculating cadence. private double totalCrankRotations; /** * Default constructor. */ public TripStatistics() { } /** * Copy constructor. * * @param other another statistics data object to copy from */ public TripStatistics(TripStatistics other) { this.startTime = other.startTime; this.stopTime = other.stopTime; this.totalDistance = other.totalDistance; this.totalTime = other.totalTime; this.movingTime = other.movingTime; this.latitudeExtremities.set( other.latitudeExtremities.getMin(), other.latitudeExtremities.getMax()); this.longitudeExtremities.set( other.longitudeExtremities.getMin(), other.longitudeExtremities.getMax()); this.maxSpeed = other.maxSpeed; this.elevationExtremities.set( other.elevationExtremities.getMin(), other.elevationExtremities.getMax()); this.totalElevationGain = other.totalElevationGain; this.gradeExtremities.set(other.gradeExtremities.getMin(), other.gradeExtremities.getMax()); this.totalWorkDone = other.totalWorkDone; this.totalHeartBeats = other.totalHeartBeats; this.totalCrankRotations = other.totalCrankRotations; } /** * Combines these statistics with those from another object. This assumes that * the time periods covered by each do not intersect. * * FIXME: If the times aren't set they default to -1. This messes up the merge. * * @param other another statistics data object */ public void merge(TripStatistics other) { startTime = Math.min(startTime, other.startTime); stopTime = Math.max(stopTime, other.stopTime); totalDistance += other.totalDistance; totalTime += other.totalTime; movingTime += other.movingTime; if (other.latitudeExtremities.hasData()) { latitudeExtremities.update(other.latitudeExtremities.getMin()); latitudeExtremities.update(other.latitudeExtremities.getMax()); } if (other.longitudeExtremities.hasData()) { longitudeExtremities.update(other.longitudeExtremities.getMin()); longitudeExtremities.update(other.longitudeExtremities.getMax()); } maxSpeed = Math.max(maxSpeed, other.maxSpeed); if (other.elevationExtremities.hasData()) { elevationExtremities.update(other.elevationExtremities.getMin()); elevationExtremities.update(other.elevationExtremities.getMax()); } totalElevationGain += other.totalElevationGain; if (other.gradeExtremities.hasData()) { gradeExtremities.update(other.gradeExtremities.getMin()); gradeExtremities.update(other.gradeExtremities.getMax()); } totalWorkDone += other.totalWorkDone; totalCrankRotations += other.totalCrankRotations; totalHeartBeats += other.totalHeartBeats; } /** * Gets the trip start time. The number of milliseconds since epoch. */ public long getStartTime() { return startTime; } /** * Gets the trip stop time. The number of milliseconds since epoch. */ public long getStopTime() { return stopTime; } /** * Gets the total distance the user traveled in meters. */ public double getTotalDistance() { return totalDistance; } /** * Gets the total time in milliseconds that this track has been active. This * statistic is only updated when a new point is added to the statistics, so * it may be off. If you need to calculate the proper total time, use * {@link #getStartTime} with the current time. */ public long getTotalTime() { return totalTime; } public double getTotalTimeSeconds() { return totalTime / CyclismoLibConstants.MILLISEC_IN_SEC; } /** * Gets the moving time in milliseconds. */ public long getMovingTime() { return movingTime; } public double getMovingTimeSeconds() { return movingTime / CyclismoLibConstants.MILLISEC_IN_SEC; } public double getMovingTimeMinutes() { return movingTime / CyclismoLibConstants.MILLISEC_IN_MIN; } /** * Gets the topmost position (highest latitude) of the track, in signed * degrees. */ public double getTopDegrees() { return latitudeExtremities.getMax(); } /** * Gets the topmost position (highest latitude) of the track, in signed * millions of degrees. */ public int getTop() { return (int) (latitudeExtremities.getMax() * 1E6); } /** * Gets the bottommost position (lowest latitude) of the track, in signed * degrees. */ public double getBottomDegrees() { return latitudeExtremities.getMin(); } /** * Gets the bottommost position (lowest latitude) of the track, in signed * millions of degrees. */ public int getBottom() { return (int) (latitudeExtremities.getMin() * 1E6); } /** * Gets the leftmost position (lowest longitude) of the track, in signed * degrees. */ public double getLeftDegrees() { return longitudeExtremities.getMin(); } /** * Gets the leftmost position (lowest longitude) of the track, in signed * millions of degrees. */ public int getLeft() { return (int) (longitudeExtremities.getMin() * 1E6); } /** * Gets the rightmost position (highest longitude) of the track, in signed * degrees. */ public double getRightDegrees() { return longitudeExtremities.getMax(); } /** * Gets the rightmost position (highest longitude) of the track, in signed * millions of degrees. */ public int getRight() { return (int) (longitudeExtremities.getMax() * 1E6); } /** * Gets the mean latitude position of the track, in signed degrees. */ public double getMeanLatitude() { return (getBottomDegrees() + getTopDegrees()) / 2.0; } /** * Gets the mean longitude position of the track, in signed degrees. */ public double getMeanLongitude() { return (getLeftDegrees() + getRightDegrees()) / 2.0; } /** * Gets the average speed in meters/second. This calculation only takes into * account the displacement until the last point that was accounted for in * statistics. */ public double getAverageSpeed() { if (totalTime == 0L) { return 0.0; } return totalDistance / getTotalTimeSeconds(); } /** * Gets the average moving speed in meters/second. */ public double getAverageMovingSpeed() { if (movingTime == 0L) { return 0.0; } return totalDistance / getMovingTimeSeconds(); } /** * Gets the maximum speed in meters/second. */ public double getMaxSpeed() { return maxSpeed; } /** * Gets the minimum elevation. This is calculated from the smoothed elevation * so this can actually be more than the current elevation. */ public double getMinElevation() { return elevationExtremities.getMin(); } /** * Gets the maximum elevation. This is calculated from the smoothed elevation * so this can actually be less than the current elevation. */ public double getMaxElevation() { return elevationExtremities.getMax(); } /** * Gets the total elevation gain in meters. This is calculated as the sum of * all positive differences in the smoothed elevation. */ public double getTotalElevationGain() { return totalElevationGain; } /** * Gets the minimum grade for this trip. */ public double getMinGrade() { return gradeExtremities.getMin(); } /** * Gets the maximum grade for this trip. */ public double getMaxGrade() { return gradeExtremities.getMax(); } /** * Gets the average cycling power over the session duration (W). */ public double getAverageMovingPower() { return totalWorkDone / getMovingTimeSeconds(); } /** * Sets the trip start time. * * @param startTime the trip start time in milliseconds since the epoch */ public void setStartTime(long startTime) { this.startTime = startTime; } /** * Sets the trip stop time. * * @param stopTime the stop time in milliseconds since the epoch */ public void setStopTime(long stopTime) { this.stopTime = stopTime; } /** * Sets the total trip distance. * * @param totalDistance the trip distance in meters */ public void setTotalDistance(double totalDistance) { this.totalDistance = totalDistance; } /** * Adds to the current total distance. * * @param distance the distance to add in meters */ void addTotalDistance(double distance) { totalDistance += distance; } /** * Sets the trip total time. * * @param totalTime the trip total time in milliseconds */ public void setTotalTime(long totalTime) { Log.d(TAG, "Total time set: " + totalTime); this.totalTime = totalTime; } /** * Sets the trip total moving time. * * @param movingTime the trip total moving time in milliseconds */ public void setMovingTime(long movingTime) { Log.d(TAG, "Moving time set: " + movingTime); this.movingTime = movingTime; } /** * Adds to the trip total moving time. * * @param time the time in milliseconds */ void addMovingTime(long time) { movingTime += time; } /** * Sets the bounding box for this trip. The unit for all parameters is signed * millions of degree (degrees * 1E6). * * @param leftE6 the leftmost longitude reached * @param topE6 the topmost latitude reached * @param rightE6 the rightmost longitude reached * @param bottomE6 the bottommost latitude reached */ public void setBounds(int leftE6, int topE6, int rightE6, int bottomE6) { latitudeExtremities.set(bottomE6 / 1E6, topE6 / 1E6); longitudeExtremities.set(leftE6 / 1E6, rightE6 / 1E6); } /** * Updates a new latitude value. * * @param latitude the latitude value in signed decimal degrees */ void updateLatitudeExtremities(double latitude) { latitudeExtremities.update(latitude); } /** * Updates a new longitude value. * * @param longitude the longitude value in signed decimal degrees */ void updateLongitudeExtremities(double longitude) { longitudeExtremities.update(longitude); } /** * Sets the maximum speed. * * @param maxSpeed the maximum speed in meters/second */ public void setMaxSpeed(double maxSpeed) { this.maxSpeed = maxSpeed; } /** * Sets the minimum elevation. * * @param elevation the minimum elevation in meters */ public void setMinElevation(double elevation) { elevationExtremities.setMin(elevation); } /** * Sets the maximum elevation. * * @param elevation the maximum elevation in meters */ public void setMaxElevation(double elevation) { elevationExtremities.setMax(elevation); } /** * Updates a new elevation. * * @param elevation the elevation value in meters */ void updateElevationExtremities(double elevation) { elevationExtremities.update(elevation); } /** * Sets the total elevation gain. * * @param totalElevationGain the elevation gain in meters */ public void setTotalElevationGain(double totalElevationGain) { this.totalElevationGain = totalElevationGain; } /** * Adds to the total elevation gain. * * @param gain the elevation gain in meters */ void addTotalElevationGain(double gain) { totalElevationGain += gain; } /** * Sets the minimum grade. * * @param grade the grade as a fraction (-1.0 would mean vertical downwards) */ public void setMinGrade(double grade) { gradeExtremities.setMin(grade); } /** * Sets the maximum grade. * * @param grade the grade as a fraction (1.0 would mean vertical upwards) */ public void setMaxGrade(double grade) { gradeExtremities.setMax(grade); } /** * Adds to the total useful work done. Used for power calculations. * * @param joules the additional work done. */ public void addWorkDone(double joules) { totalWorkDone += joules; Log.d(TAG, "Useful work done: " + totalWorkDone); } public void setTotalWorkDone(double joules) { totalWorkDone = joules; } public double getTotalWorkDone() { return totalWorkDone; } /** * Add heart beats to the running total. Used for working out average heart rate. * * @param heartBeats to add to the total. */ public void addHeartBeats(double heartBeats) { totalHeartBeats += heartBeats; } public double getTotalHeartBeats() { return totalHeartBeats; } /** * @return average moving heart rate in beats per minute to the nearest whole number. */ public int getAverageMovingHeartRate() { return (int) Math.round(totalHeartBeats / getMovingTimeMinutes()); } public void setTotalHeartBeats(double totalHeartBeats) { this.totalHeartBeats = totalHeartBeats; } /** * Add crank revolutions. Used for working out average cadence. * * @param crankRotations to add to the total. */ public void addCrankRotations(double crankRotations) { totalCrankRotations += crankRotations; } public double getTotalCrankRotations() { return totalCrankRotations; } /** * @return average moving cadence to the nearest whole number in revs/minute. */ public int getAverageMovingCadence() { return (int) Math.round(totalCrankRotations / getMovingTimeMinutes()); } public void setTotalCrankRotations(double totalCrankRotations) { this.totalCrankRotations = totalCrankRotations; } /** * Updates a new grade value. * * @param grade the grade value as a fraction */ void updateGradeExtremities(double grade) { gradeExtremities.update(grade); } @Override public String toString() { return "TripStatistics { Start Time: " + getStartTime() + "; Stop Time: " + getStopTime() + "; Total Time: " + getTotalTime() + "; Total Distance: " + getTotalDistance() + "; Total Time: " + getTotalTime() + "; Moving Time: " + getMovingTime() + "; Min Latitude: " + getBottomDegrees() + "; Max Latitude: " + getTopDegrees() + "; Min Longitude: " + getLeftDegrees() + "; Max Longitude: " + getRightDegrees() + "; Max Elevation: " + getMaxElevation() + "; Max Speed: " + getMaxSpeed() + "; Min Elevation: " + getMinElevation() + "; Max Elevation: " + getMaxElevation() + "; Elevation Gain: " + getTotalElevationGain() + "; Min Grade: " + getMinGrade() + "; Max Grade: " + getMaxGrade() + "; Total work done: " + getTotalWorkDone() + "; Total heart beats: " + getTotalHeartBeats() + "; Total crank rotations: " + getTotalCrankRotations() + "}"; } /** * Creator of statistics data from parcels. */ public static class Creator implements Parcelable.Creator<TripStatistics> { @Override public TripStatistics createFromParcel(Parcel source) { TripStatistics data = new TripStatistics(); data.startTime = source.readLong(); data.stopTime = source.readLong(); data.totalDistance = source.readDouble(); data.totalTime = source.readLong(); data.movingTime = source.readLong(); double minLat = source.readDouble(); double maxLat = source.readDouble(); data.latitudeExtremities.set(minLat, maxLat); double minLong = source.readDouble(); double maxLong = source.readDouble(); data.longitudeExtremities.set(minLong, maxLong); data.totalWorkDone = source.readDouble(); data.totalHeartBeats = source.readDouble(); data.totalCrankRotations = source.readDouble(); data.maxSpeed = source.readDouble(); double minElev = source.readDouble(); double maxElev = source.readDouble(); data.elevationExtremities.set(minElev, maxElev); data.totalElevationGain = source.readDouble(); double minGrade = source.readDouble(); double maxGrade = source.readDouble(); data.gradeExtremities.set(minGrade, maxGrade); return data; } @Override public TripStatistics[] newArray(int size) { return new TripStatistics[size]; } } /** * Creator of {@link TripStatistics} from parcels. */ public static final Creator CREATOR = new Creator(); @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(startTime); dest.writeLong(stopTime); dest.writeDouble(totalDistance); dest.writeLong(totalTime); dest.writeLong(movingTime); dest.writeDouble(latitudeExtremities.getMin()); dest.writeDouble(latitudeExtremities.getMax()); dest.writeDouble(longitudeExtremities.getMin()); dest.writeDouble(longitudeExtremities.getMax()); dest.writeDouble(totalWorkDone); dest.writeDouble(totalHeartBeats); dest.writeDouble(totalCrankRotations); dest.writeDouble(maxSpeed); dest.writeDouble(elevationExtremities.getMin()); dest.writeDouble(elevationExtremities.getMax()); dest.writeDouble(totalElevationGain); dest.writeDouble(gradeExtremities.getMin()); dest.writeDouble(gradeExtremities.getMax()); } }