+package tim.prune.data;
+
+import tim.prune.config.Config;
+
+/**
+ * Class to do calculations of range statistics such as distances, durations,
+ * speeds, gradients etc, and to hold the results of the calculations.
+ * Used by FullRangeDetails as well as the EstimateTime functions.
+ */
+public class RangeStats
+{
+ private boolean _valid = false;
+ private int _numPoints = 0;
+ private int _startIndex = 0, _endIndex = 0;
+ private int _numSegments = 0;
+ private AltitudeRange _totalAltitudeRange = null, _movingAltitudeRange = null;
+ private AltitudeRange _gentleAltitudeRange = null, _steepAltitudeRange = null;
+ private Timestamp _earliestTimestamp = null, _latestTimestamp = null;
+ private long _movingMilliseconds = 0L;
+ private boolean _timestampsIncomplete = false;
+ private double _totalDistanceRads = 0.0, _movingDistanceRads = 0.0;
+ // Note, maximum speed is not calculated here, use the SpeedData method instead
+
+ private static final double STEEP_ANGLE = 0.15; // gradient steeper than 15% counts as steep
+
+
+ /**
+ * Constructor
+ * @param inTrack track to compile data for
+ * @param inStartIndex start index of range to examine
+ * @param inEndIndex end index (inclusive) of range to examine
+ */
+ public RangeStats(Track inTrack, int inStartIndex, int inEndIndex)
+ {
+ if (inTrack != null && inStartIndex >= 0 && inEndIndex > inStartIndex
+ && inEndIndex < inTrack.getNumPoints())
+ {
+ _valid = calculateStats(inTrack, inStartIndex, inEndIndex);
+ }
+ }
+
+ /**
+ * Calculate the statistics and populate the member variables with the results
+ * @param inTrack track
+ * @param inStartIndex start index of range
+ * @param inEndIndex end index (inclusive) of range
+ * @return true on success
+ */
+ private boolean calculateStats(Track inTrack, int inStartIndex, int inEndIndex)
+ {
+ _startIndex = inStartIndex; _endIndex = inEndIndex;
+ _numPoints = inEndIndex - inStartIndex + 1;
+ _totalAltitudeRange = new AltitudeRange();
+ _movingAltitudeRange = new AltitudeRange();
+ _gentleAltitudeRange = new AltitudeRange();
+ _steepAltitudeRange = new AltitudeRange();
+ DataPoint prevPoint = null;
+ Altitude prevAltitude = null;
+ _totalDistanceRads = _movingDistanceRads = 0.0;
+ double radsSinceLastAltitude = 0.0;
+ _movingMilliseconds = 0L;
+
+ // Loop over the points in the range
+ for (int i=inStartIndex; i<= inEndIndex; i++)
+ {
+ DataPoint p = inTrack.getPoint(i);
+ if (p == null) return false;
+ // ignore all waypoints
+ if (p.isWaypoint()) continue;
+
+ if (p.getSegmentStart()) {_numSegments++;}
+ // Get the distance to the previous track point
+ if (prevPoint != null)
+ {
+ double rads = DataPoint.calculateRadiansBetween(prevPoint, p);
+ _totalDistanceRads += rads;
+ if (!p.getSegmentStart()) {
+ _movingDistanceRads += rads;
+ }
+ // Keep track of rads since last point with an altitude
+ radsSinceLastAltitude += rads;
+ }
+ // Get the altitude difference to the previous track point
+ if (p.hasAltitude())
+ {
+ Altitude altitude = p.getAltitude();
+ _totalAltitudeRange.addValue(altitude);
+ if (p.getSegmentStart()) {
+ _movingAltitudeRange.ignoreValue(altitude);
+ }
+ else
+ {
+ _movingAltitudeRange.addValue(altitude);
+ if (prevAltitude != null)
+ {
+ // Work out gradient, see whether to ignore/add to gentle or steep
+ double heightDiff = altitude.getMetricValue() - prevAltitude.getMetricValue();
+ double metricDist = Distance.convertRadiansToDistance(radsSinceLastAltitude, UnitSetLibrary.UNITS_METRES);
+ final boolean isSteep = metricDist < 0.001 || (Math.abs(heightDiff / metricDist) > STEEP_ANGLE);
+ if (isSteep) {
+ _steepAltitudeRange.ignoreValue(prevAltitude);
+ _steepAltitudeRange.addValue(altitude);
+ }
+ else {
+ _gentleAltitudeRange.ignoreValue(prevAltitude);
+ _gentleAltitudeRange.addValue(altitude);
+ }
+ }
+ }
+ prevAltitude = altitude;
+ radsSinceLastAltitude = 0.0;
+ }
+
+ if (p.hasTimestamp())
+ {
+ if (_earliestTimestamp == null || p.getTimestamp().isBefore(_earliestTimestamp)) {
+ _earliestTimestamp = p.getTimestamp();
+ }
+ if (_latestTimestamp == null || p.getTimestamp().isAfter(_latestTimestamp)) {
+ _latestTimestamp = p.getTimestamp();
+ }
+ // Work out duration without segment gaps
+ if (!p.getSegmentStart() && prevPoint != null && prevPoint.hasTimestamp())
+ {
+ long millisLater = p.getTimestamp().getMillisecondsSince(prevPoint.getTimestamp());
+ if (millisLater < 0) {_timestampsIncomplete = true;}
+ _movingMilliseconds += millisLater;
+ }
+ }
+ else if (!p.getSegmentStart()) {
+ _timestampsIncomplete = true;
+ }
+
+ prevPoint = p;
+ }
+ return true;
+ }
+
+
+ /** @return true if results are valid */
+ public boolean isValid() {
+ return _valid;
+ }
+
+ /** @return start index of range */
+ public int getStartIndex() {
+ return _startIndex;
+ }
+
+ /** @return end index of range */
+ public int getEndIndex() {
+ return _endIndex;
+ }
+
+ /** @return number of points in range */
+ public int getNumPoints() {
+ return _numPoints;
+ }
+
+ /** @return number of segments in range */
+ public int getNumSegments() {
+ return _numSegments;
+ }
+
+ /** @return altitude range of range including segment gaps */
+ public AltitudeRange getTotalAltitudeRange() {
+ return _totalAltitudeRange;
+ }
+
+ /** @return altitude range of range just within segments */
+ public AltitudeRange getMovingAltitudeRange() {
+ return _movingAltitudeRange;
+ }
+
+ /** @return altitude range of range just considering low gradient bits */
+ public AltitudeRange getGentleAltitudeRange() {
+ return _gentleAltitudeRange;
+ }
+
+ /** @return altitude range of range just considering high gradient bits */
+ public AltitudeRange getSteepAltitudeRange() {
+ return _steepAltitudeRange;
+ }
+
+ /** @return the earliest timestamp found */
+ public Timestamp getEarliestTimestamp() {
+ return _earliestTimestamp;
+ }
+
+ /** @return the latest timestamp found */
+ public Timestamp getLatestTimestamp() {
+ return _latestTimestamp;
+ }
+
+ /** @return total number of seconds in the range */
+ public long getTotalDurationInSeconds()
+ {
+ if (_earliestTimestamp != null && _latestTimestamp != null) {
+ return _latestTimestamp.getSecondsSince(_earliestTimestamp);
+ }
+ return 0L;
+ }
+
+ /** @return number of seconds within the segments of the range */
+ public long getMovingDurationInSeconds()
+ {
+ return _movingMilliseconds / 1000;
+ }
+
+ /** @return true if any timestamps are missing or out of sequence */
+ public boolean getTimestampsIncomplete() {
+ return _timestampsIncomplete;
+ }
+
+ /** @return total distance in the current distance units (km or mi) */
+ public double getTotalDistance() {
+ return Distance.convertRadiansToDistance(_totalDistanceRads);
+ }
+
+ /** @return moving distance in the current distance units (km or mi) */
+ public double getMovingDistance() {
+ return Distance.convertRadiansToDistance(_movingDistanceRads);
+ }
+
+ /** @return moving distance in km */
+ public double getMovingDistanceKilometres() {
+ return Distance.convertRadiansToDistance(_movingDistanceRads, UnitSetLibrary.UNITS_KILOMETRES);
+ }
+
+ /** @return the total gradient in % (including segment gaps) */
+ public double getTotalGradient()
+ {
+ double dist = Distance.convertRadiansToDistance(_totalDistanceRads, UnitSetLibrary.UNITS_METRES);
+ if (dist > 0.0 && _totalAltitudeRange.hasRange()) {
+ return _totalAltitudeRange.getMetricHeightDiff() / dist * 100.0;
+ }
+ return 0.0;
+ }
+
+ /** @return the moving gradient in % (ignoring segment gaps) */
+ public double getMovingGradient()
+ {
+ double dist = Distance.convertRadiansToDistance(_movingDistanceRads, UnitSetLibrary.UNITS_METRES);
+ if (dist > 0.0 && _movingAltitudeRange.hasRange()) {
+ return _movingAltitudeRange.getMetricHeightDiff() / dist * 100.0;
+ }
+ return 0.0;
+ }
+
+ /** @return the total vertical speed (including segment gaps) in current vspeed units */
+ public double getTotalVerticalSpeed()
+ {
+ long time = getTotalDurationInSeconds();
+ if (time > 0 && _totalAltitudeRange.hasRange()) {
+ return _totalAltitudeRange.getMetricHeightDiff() / time * Config.getUnitSet().getVerticalSpeedUnit().getMultFactorFromStd();
+ }
+ return 0.0;
+ }
+
+ /** @return the moving vertical speed (ignoring segment gaps) in current vspeed units */
+ public double getMovingVerticalSpeed()
+ {
+ long time = getMovingDurationInSeconds();
+ if (time > 0 && _movingAltitudeRange.hasRange()) {
+ return _movingAltitudeRange.getMetricHeightDiff() / time * Config.getUnitSet().getVerticalSpeedUnit().getMultFactorFromStd();
+ }
+ return 0.0;
+ }
+}