content: package tim.prune.data;
  
  
  /**
   * Class to hold static calculation functions
   * for horizontal and vertical speeds.
   * Subclasses may introduce special handling of ignoring points
   * (for example, if marked for deletion)
   */
  public class SpeedCalculator
  {
  	/**
  	 * Calculate the horizontal speed value of the track at the specified index
  	 * @param inTrack track object
  	 * @param inIndex index of point to calculate speed for
  	 * @param inUnitSet unit set to use for calculations
  	 * @param inValue object in which to place result of calculation
  	 */
  	public static void calculateSpeed(Track inTrack, int inIndex,
  		UnitSet inUnitSet, SpeedValue inValue)
  	{
  		new SpeedCalculator().calculateHorizontalSpeed(inTrack, inIndex, inUnitSet, inValue);
  	}
  
  	/**
  	 * Calculate the horizontal speed value of the track at the specified index
  	 * @param inTrack track object
  	 * @param inIndex index of point to calculate speed for
  	 * @param inUnitSet unit set to use for calculations
  	 * @param inValue object in which to place result of calculation
  	 */
  	public void calculateHorizontalSpeed(Track inTrack, int inIndex,
  		UnitSet inUnitSet, SpeedValue inValue)
  	{
  		if (inValue != null) {
  			inValue.setInvalid();
  		}
  		if (inTrack == null || inIndex < 0 || inValue == null)
  		{
  			System.err.println("Cannot calculate speed for index " + inIndex);
  			return;
  		}
  
  		DataPoint point = inTrack.getPoint(inIndex);
  		if (point == null) {return;}
  		boolean pointHasSpeed = false;
  		double  speedValue = 0.0;
  
  		// First, see if point has a speed value already
  		if (point.hasHSpeed()) {
  			speedValue = point.getHSpeed().getValue(inUnitSet.getSpeedUnit());
  			pointHasSpeed = true;
  		}
  
  		// otherwise, see if we can calculate it from the timestamps
  		if (!pointHasSpeed && point.hasTimestamp() && !point.isWaypoint())
  		{
  			double totalRadians = 0.0;
  			int index = inIndex-1;
  			DataPoint q = point;
  			Timestamp earlyStamp = point.getTimestamp();
  			boolean stop = false;
  
  			// Count backwards until timestamp earlier than now; total distances back to this point
  			if (!point.getSegmentStart())
  			{
  				do
  				{
  					if (shouldIgnorePoint(index)) {
  						stop = index < 0;
  					}
  					else
  					{
  						DataPoint p = inTrack.getPoint(index);
  						boolean timeOk = p != null && p.hasTimestamp() && p.getTimestamp().isBefore(point.getTimestamp());
  						boolean pValid = timeOk && !p.isWaypoint();
  						if (pValid) {
  							totalRadians += DataPoint.calculateRadiansBetween(p, q);
  							earlyStamp = p.getTimestamp();
  						}
  
  						stop = (p == null) || p.getSegmentStart() || hasSufficientTimeDifference(p, point);
  						if (p != null && !p.isWaypoint()) {
  							q = p;
  						}
  					}
  					index--;
  				}
  				while (!stop);
  			}
  			// Count forwards until timestamp later than now; total distances forward to this point
  			Timestamp lateStamp = point.getTimestamp();
  			q = point;
  			index = inIndex+1;
  			do
  			{
  				DataPoint p = inTrack.getPoint(index);
  				if (shouldIgnorePoint(index)) {
  					stop = (p == null);
  				}
  				else
  				{
  					boolean timeOk = p != null && p.hasTimestamp() && !p.getTimestamp().isBefore(point.getTimestamp());
  					boolean pValid = timeOk && !p.isWaypoint() && !p.getSegmentStart();
  					if (pValid) {
  						totalRadians += DataPoint.calculateRadiansBetween(p, q);
  						lateStamp = p.getTimestamp();
  					}
  
  					stop = (p == null) || p.getSegmentStart() || hasSufficientTimeDifference(point, p);
  					if (p != null && !p.isWaypoint()) {
  						q = p;
  					}
  				}
  				index++;
  			}
  			while (!stop);
  
  			// See if we've managed to get a time range of at least a second
  			long milliseconds = lateStamp.getMillisecondsSince(earlyStamp);
  			if (milliseconds >= 1000L)
  			{
  				double dist = Distance.convertRadiansToDistance(totalRadians, inUnitSet.getDistanceUnit());
  				speedValue = dist / milliseconds * 1000.0 * 60.0 * 60.0; // convert from per millisec to per hour
  				pointHasSpeed = true;
  			}
  		}
  		// Did we get a value?
  		if (pointHasSpeed) {
  			inValue.setValue(speedValue);
  		}
  		// otherwise, just leave value as invalid
  	}
  
  
  	/**
  	 * Subclasses may override this to ignore certain points
  	 * @return true to ignore the specified point, false to consider it
  	 */
  	protected boolean shouldIgnorePoint(int index) {
  		return false;
  	}
  
  
  	/**
  	 * Calculate the vertical speed value of the track at the specified index
  	 * @param inTrack track object
  	 * @param inIndex index of point to calculate speed for
  	 * @param inUnitSet unit set to use for calculations
  	 * @param inValue object in which to place the result of calculation
  	 */
  	public static void calculateVerticalSpeed(Track inTrack, int inIndex,
  		UnitSet inUnitSet, SpeedValue inValue)
  	{
  		if (inTrack == null || inIndex < 0 || inValue == null) {
  			System.err.println("Cannot calculate vert speed for index " + inIndex);
  			return;
  		}
  		inValue.setInvalid();
  
  		DataPoint point = inTrack.getPoint(inIndex);
  		boolean pointHasSpeed = false;
  		double  speedValue = 0.0;
  
  		// First, see if point has a speed value already
  		if (point != null && point.hasVSpeed())
  		{
  			speedValue = point.getVSpeed().getValue(inUnitSet.getVerticalSpeedUnit());
  			pointHasSpeed = true;
  		}
  		// otherwise, see if we can calculate it from the heights and timestamps
  		if (!pointHasSpeed
  			&& point != null && point.hasTimestamp() && point.hasAltitude() && !point.isWaypoint())
  		{
  			int index = inIndex-1;
  			Timestamp earlyStamp = point.getTimestamp();
  			Altitude firstAlt = point.getAltitude();
  			boolean stop = false;
  
  			// Count backwards until timestamp earlier than now
  			if (!point.getSegmentStart())
  			{
  				do
  				{
  					DataPoint p = inTrack.getPoint(index);
  					boolean timeOk = p != null && p.hasTimestamp() && p.getTimestamp().isBefore(point.getTimestamp());
  					boolean pValid = timeOk && !p.isWaypoint();
  					if (pValid) {
  						earlyStamp = p.getTimestamp();
  						if (p.hasAltitude()) firstAlt = p.getAltitude();
  					}
  
  					stop = (p == null) || p.getSegmentStart() || hasSufficientTimeDifference(p, point);
  					index--;
  				}
  				while (!stop);
  			}
  
  			// Count forwards until timestamp later than now
  			Timestamp lateStamp = point.getTimestamp();
  			Altitude lastAlt = point.getAltitude();
  			index = inIndex+1;
  			do
  			{
  				DataPoint p = inTrack.getPoint(index);
  				boolean timeOk = p != null && p.hasTimestamp() && !p.getTimestamp().isBefore(point.getTimestamp());
  				boolean pValid = timeOk && !p.isWaypoint() && !p.getSegmentStart();
  				if (pValid) {
  					lateStamp = p.getTimestamp();
  					if (p.hasAltitude()) lastAlt = p.getAltitude();
  				}
  
  				stop = (p == null) || p.getSegmentStart() || hasSufficientTimeDifference(point, p);
  				index++;
  			}
  			while (!stop);
  
  			// See if we've managed to get a non-zero time range
  			long milliseconds = lateStamp.getMillisecondsSince(earlyStamp);
  			if (milliseconds >= 1000L)
  			{
  				double altDiff = (lastAlt.getMetricValue() - firstAlt.getMetricValue())
  				 * inUnitSet.getVerticalSpeedUnit().getMultFactorFromStd();
  				speedValue = altDiff / milliseconds * 1000.0; // units are feet/sec or metres/sec
  				pointHasSpeed = true;
  			}
  		}
  		// Check whether we got a value from either method
  		if (pointHasSpeed) {
  			inValue.setValue(speedValue);
  		}
  	}
  
  	/**
  	 * Check whether the time difference between P1 and P2 is sufficiently large
  	 * @param inP1 earlier point
  	 * @param inP2 later point
  	 * @return true if we can stop looking now, found a point early/late enough
  	 */
  	private static boolean hasSufficientTimeDifference(DataPoint inP1, DataPoint inP2)
  	{
  		if (inP1 == null || inP2 == null) {
  			return true; // we have to give up now
  		}
  		if (!inP1.hasTimestamp() || !inP2.hasTimestamp()) {
  			return false; // keep looking
  		}
  		final long MIN_TIME_DIFFERENCE_MS = 1000L;
  		return inP2.getTimestamp().getMillisecondsSince(inP1.getTimestamp()) >= MIN_TIME_DIFFERENCE_MS;
  	}
  }
  