content: package tim.prune.data;
  
  import java.util.List;
  
  import tim.prune.UpdateMessageBroker;
  import tim.prune.gui.map.MapUtils;
  
  
  /**
   * Class to hold all track information,
   * including track points and waypoints
   */
  public class Track
  {
  	// Data points
  	private DataPoint[] _dataPoints;
  	// Scaled x, y values
  	private double[] _xValues = null;
  	private double[] _yValues = null;
  	private boolean _scaled = false;
  	private int _numPoints;
  	private boolean _hasTrackpoint = false;
  	private boolean _hasWaypoint = false;
  	private FieldList _masterFieldList;
  	// variable ranges
  	private DoubleRange _latRange = null, _longRange = null;
  	private DoubleRange _xRange = null, _yRange = null;
  
  
  	/**
  	 * Constructor for empty track
  	 */
  	public Track()
  	{
  		// create field list
  		_masterFieldList = new FieldList();
  		// make empty DataPoint array
  		_dataPoints = new DataPoint[0];
  		_numPoints = 0;
  	}
  
  	/**
  	 * Constructor using fields and points from another Track
  	 * @param inFieldList Field list from another Track object
  	 * @param inPoints (edited) point array
  	 */
  	public Track(FieldList inFieldList, DataPoint[] inPoints)
  	{
  		_masterFieldList = inFieldList;
  		_dataPoints = inPoints;
  		if (_dataPoints == null) {
  			_dataPoints = new DataPoint[0];
  		}
  		_numPoints = _dataPoints.length;
  	}
  
  	/**
  	 * Request that a rescale be done to recalculate derived values
  	 */
  	public void requestRescale() {
  		_scaled = false;
  	}
  
  	/**
  	 * Extend the track's field list with the given additional fields
  	 * @param inFieldList list of fields to be added
  	 */
  	private void extendFieldList(FieldList inFieldList) {
  		_masterFieldList = _masterFieldList.merge(inFieldList);
  	}
  
  	////////////////// Modification methods //////////////////////
  
  
  	/**
  	 * Crop the track to the given size - subsequent points are not (yet) deleted
  	 * @param inNewSize new number of points in track
  	 */
  	public void cropTo(int inNewSize)
  	{
  		if (inNewSize >= 0 && inNewSize < getNumPoints())
  		{
  			_numPoints = inNewSize;
  			// needs to be scaled again
  			_scaled = false;
  			UpdateMessageBroker.informSubscribers();
  		}
  	}
  
  	/**
  	 * Delete the specified point
  	 * @param inIndex point index
  	 * @return true if successful
  	 */
  	public boolean deletePoint(int inIndex)
  	{
  		DataPoint point = getPoint(inIndex);
  		if (point == null) {
  			return false;
  		}
  		if (point.getSegmentStart())
  		{
  			DataPoint nextTrackPoint = getNextTrackPoint(inIndex+1);
  			if (nextTrackPoint != null) {
  				nextTrackPoint.setSegmentStart(true);
  			}
  		}
  		DataPoint[] newPointArray = new DataPoint[_numPoints - 1];
  		// Copy points before the selected point
  		if (inIndex > 0) {
  			System.arraycopy(_dataPoints, 0, newPointArray, 0, inIndex);
  		}
  		// Copy points after
  		int numAfter = _numPoints - inIndex - 1;
  		if (numAfter > 0) {
  			System.arraycopy(_dataPoints, inIndex + 1, newPointArray, inIndex, numAfter);
  		}
  		// Copy points over original array
  		_dataPoints = newPointArray;
  		_numPoints --;
  		// needs to be scaled again
  		_scaled = false;
  		return true;
  	}
  
  	/**
  	 * Rearrange all the points in the track according to the given list
  	 * @param inIndexes point indexes for new ordering
  	 */
  	public boolean rearrangePoints(List<Integer> inIndexes)
  	{
  		if (inIndexes == null || inIndexes.size() != _numPoints) {
  			return false;
  		}
  		DataPoint[] newPointArray = new DataPoint[_numPoints];
  		// Move points around
  		for (int i=0; i<_numPoints; i++) {
  			newPointArray[i] = getPoint(inIndexes.get(i));
  		}
  		// Copy array references
  		_dataPoints = newPointArray;
  		_scaled = false;
  		return true;
  	}
  
  
  	//////// information methods /////////////
  
  
  	/**
  	 * Get the point at the given index
  	 * @param inPointNum index number, starting at 0
  	 * @return DataPoint object, or null if out of range
  	 */
  	public DataPoint getPoint(int inPointNum)
  	{
  		if (inPointNum > -1 && inPointNum < getNumPoints()) {
  			return _dataPoints[inPointNum];
  		}
  		return null;
  	}
  
  	/**
  	 * @return the number of (valid) points in the track
  	 */
  	public int getNumPoints() {
  		return _numPoints;
  	}
  
  	/**
  	 * @return The range of x values as a DoubleRange object
  	 */
  	public DoubleRange getXRange()
  	{
  		if (!_scaled) {scalePoints();}
  		return _xRange;
  	}
  
  	/**
  	 * @return The range of y values as a DoubleRange object
  	 */
  	public DoubleRange getYRange()
  	{
  		if (!_scaled) {scalePoints();}
  		return _yRange;
  	}
  
  	/**
  	 * @return The range of lat values as a DoubleRange object
  	 */
  	public DoubleRange getLatRange()
  	{
  		if (!_scaled) {scalePoints();}
  		return _latRange;
  	}
  	/**
  	 * @return The range of lon values as a DoubleRange object
  	 */
  	public DoubleRange getLonRange()
  	{
  		if (!_scaled) {scalePoints();}
  		return _longRange;
  	}
  
  	/**
  	 * @param inPointNum point index, starting at 0
  	 * @return scaled x value of specified point
  	 */
  	public double getX(int inPointNum)
  	{
  		if (!_scaled) {scalePoints();}
  		return _xValues[inPointNum];
  	}
  
  	/**
  	 * @param inPointNum point index, starting at 0
  	 * @return scaled y value of specified point
  	 */
  	public double getY(int inPointNum)
  	{
  		if (!_scaled) {scalePoints();}
  		return _yValues[inPointNum];
  	}
  
  	/**
  	 * @return the master field list
  	 */
  	public FieldList getFieldList() {
  		return _masterFieldList;
  	}
  
  
  	/**
  	 * Checks if any data exists for the specified field
  	 * @param inField Field to examine
  	 * @return true if data exists for this field
  	 */
  	public boolean hasData(Field inField) {
  		return hasData(inField, 0, _numPoints-1);
  	}
  
  
  	/**
  	 * Checks if any data exists for the specified field in the specified range
  	 * @param inField Field to examine
  	 * @param inStart start of range to check
  	 * @param inEnd end of range to check (inclusive)
  	 * @return true if data exists for this field
  	 */
  	public boolean hasData(Field inField, int inStart, int inEnd)
  	{
  		// Loop over selected point range
  		for (int i=inStart; i<=inEnd; i++)
  		{
  			DataPoint point = getPoint(i);
  			if (point == null) {
  				continue;
  			}
  			final boolean hasValue;
  			if (inField == Field.ALTITUDE) {
  				hasValue = point.hasAltitude();
  			}
  			else if (inField == Field.TIMESTAMP) {
  				hasValue = point.hasTimestamp();
  			}
  			else if (inField == Field.PHOTO) {
  				hasValue = point.getPhoto() != null;
  			}
  			else if (inField == Field.AUDIO) {
  				hasValue = point.getAudio() != null;
  			}
  			else {
  				hasValue = point.getFieldValue(inField) != null;
  			}
  			if (hasValue) {
  				return true;
  			}
  		}
  		return false;
  	}
  
  	/**
  	 * @return true if track has altitude data
  	 */
  	public boolean hasAltitudeData() {
  		return hasData(Field.ALTITUDE);
  	}
  
  	/**
  	 * @return true if track contains at least one trackpoint
  	 */
  	public boolean hasTrackPoints()
  	{
  		if (!_scaled) {scalePoints();}
  		return _hasTrackpoint;
  	}
  
  	/**
  	 * @return true if track contains waypoints
  	 */
  	public boolean hasWaypoints()
  	{
  		if (!_scaled) {scalePoints();}
  		return _hasWaypoint;
  	}
  
  	/**
  	 * Collect all the waypoints into the given List
  	 * @param inList List to fill with waypoints
  	 */
  	public void getWaypoints(List<DataPoint> inList)
  	{
  		// clear list
  		inList.clear();
  		// loop over points and copy all waypoints into list
  		for (int i=0; i<=_numPoints-1; i++)
  		{
  			if (_dataPoints[i] != null && _dataPoints[i].isWaypoint())
  			{
  				inList.add(_dataPoints[i]);
  			}
  		}
  	}
  
  
  	/**
  	 * Search for the given Point in the track and return the index
  	 * @param inPoint Point to look for
  	 * @return index of Point, if any or -1 if not found
  	 */
  	public int getPointIndex(DataPoint inPoint)
  	{
  		if (inPoint != null)
  		{
  			// Loop over points in track
  			for (int i=0; i<=_numPoints-1; i++)
  			{
  				if (_dataPoints[i] == inPoint) {
  					return i;
  				}
  			}
  		}
  		// not found
  		return -1;
  	}
  
  	/**
  	 * @return true if all the points in the track have the same source file
  	 */
  	public boolean hasSingleSourceFile()
  	{
  		SourceInfo prevInfo = null;
  		for (int p=0; p < getNumPoints(); p++)
  		{
  			DataPoint point = getPoint(p);
  			SourceInfo info = (point == null ? null : point.getSourceInfo());
  			if (info == null || (prevInfo != null && info != prevInfo)) {
  				return false;
  			}
  			if (info.getFile() == null) {
  				return false;
  			}
  			prevInfo = info;
  		}
  		return true;
  	}
  
  	///////// Internal processing methods ////////////////
  
  
  	/**
  	 * Scale all the points in the track to gain x and y values
  	 * ready for plotting
  	 */
  	private synchronized void scalePoints()
  	{
  		// Loop through all points in track, to see limits of lat, long
  		_longRange = new DoubleRange();
  		_latRange = new DoubleRange();
  		_hasWaypoint = false; _hasTrackpoint = false;
  		for (int p=0; p < getNumPoints(); p++)
  		{
  			DataPoint point = getPoint(p);
  			if (point != null && point.isValid())
  			{
  				_longRange.addValue(point.getLongitude().getDouble());
  				_latRange.addValue(point.getLatitude().getDouble());
  				if (point.isWaypoint()) {
  					_hasWaypoint = true;
  				}
  				else {
  					_hasTrackpoint = true;
  				}
  			}
  		}
  
  		// Loop over points and calculate scales
  		_xValues = new double[getNumPoints()];
  		_yValues = new double[getNumPoints()];
  		_xRange = new DoubleRange();
  		_yRange = new DoubleRange();
  		for (int p=0; p < getNumPoints(); p++)
  		{
  			DataPoint point = getPoint(p);
  			if (point != null)
  			{
  				_xValues[p] = MapUtils.getXFromLongitude(point.getLongitude().getDouble());
  				_xRange.addValue(_xValues[p]);
  				_yValues[p] = MapUtils.getYFromLatitude(point.getLatitude().getDouble());
  				_yRange.addValue(_yValues[p]);
  			}
  		}
  		_scaled = true;
  	}
  
  	/**
  	 * Find the nearest track point to the specified point
  	 * @param inPointIndex index of point within track
  	 * @return point index of nearest track point
  	 */
  	public int getNearestTrackPointIndex(int inPointIndex) {
  		return getNearestPointIndex(_xValues[inPointIndex], _yValues[inPointIndex], -1.0, true);
  	}
  
  	/**
  	 * Find the nearest point to the specified x and y coordinates
  	 * or -1 if no point is within the specified max distance
  	 * @param inX x coordinate
  	 * @param inY y coordinate
  	 * @param inMaxDist maximum distance from selected coordinates
  	 * @param inJustTrackPoints true if waypoints should be ignored
  	 * @return index of nearest point or -1 if not found
  	 */
  	public int getNearestPointIndex(double inX, double inY, double inMaxDist, boolean inJustTrackPoints)
  	{
  		int nearestPoint = 0;
  		double nearestDist = -1.0;
  		double mDist, yDist;
  		try {
  			for (int i=0; i < getNumPoints(); i++)
  			{
  				if (!inJustTrackPoints || !_dataPoints[i].isWaypoint())
  				{
  					yDist = Math.abs(_yValues[i] - inY);
  					if (yDist < nearestDist || nearestDist < 0.0)
  					{
  						// y dist is within range, so check x too
  						mDist = yDist + getMinXDist(_xValues[i] - inX);
  						if (mDist < nearestDist || nearestDist < 0.0)
  						{
  							nearestPoint = i;
  							nearestDist = mDist;
  						}
  					}
  				}
  			}
  		} catch (ArrayIndexOutOfBoundsException obe) {
  			return -1; // probably moving the mouse while data is changing
  		}
  		// Check whether it's within required distance
  		if (nearestDist > inMaxDist && inMaxDist > 0.0) {
  			return -1;
  		}
  		return nearestPoint;
  	}
  
  	/**
  	 * @param inX x value of point
  	 * @return minimum wrapped value
  	 */
  	private static double getMinXDist(double inX) {
  		return Math.min(Math.min(Math.abs(inX), Math.abs(inX-1.0)), Math.abs(inX+1.0));
  	}
  
  	/**
  	 * Get the next track point starting from the given index
  	 * @param inStartIndex index to start looking from
  	 * @return next track point, or null if end of data reached
  	 */
  	public DataPoint getNextTrackPoint(int inStartIndex) {
  		return getNextTrackPoint(inStartIndex, _numPoints, true);
  	}
  
  	/**
  	 * Get the next track point starting from the given index
  	 * @param inStartIndex index to start looking from
  	 * @param inEndIndex index to stop looking (inclusive)
  	 * @return next track point, or null if end of data reached
  	 */
  	public DataPoint getFirstTrackPointBetween(int inStartIndex, int inEndIndex) {
  		return getNextTrackPoint(inStartIndex, inEndIndex, true);
  	}
  
  	/**
  	 * Get the previous track point starting from the given index
  	 * @param inStartIndex index to start looking from
  	 * @return next track point, or null if end of data reached
  	 */
  	public DataPoint getPreviousTrackPoint(int inStartIndex)
  	{
  		// end index is given as _numPoints but actually it just counts down to -1
  		return getNextTrackPoint(inStartIndex, _numPoints, false);
  	}
  
  	/**
  	 * Get the next track point starting from the given index
  	 * @param inStartIndex index to start looking from
  	 * @param inEndIndex index to stop looking (inclusive)
  	 * @param inCountUp true for next, false for previous
  	 * @return next track point, or null if end of data reached
  	 */
  	private DataPoint getNextTrackPoint(int inStartIndex, int inEndIndex, boolean inCountUp)
  	{
  		int increment = inCountUp ? 1 : -1;
  		for (int i=inStartIndex; i<=inEndIndex; i+=increment)
  		{
  			DataPoint point = getPoint(i);
  			// Exit if end of data reached - there wasn't a track point
  			if (point == null) {
  				return null;
  			}
  			if (point.isValid() && !point.isWaypoint()) {
  				// next track point found
  				return point;
  			}
  		}
  		return null;
  	}
  
  	/**
  	 * @param inStartIndex start index of range
  	 * @param inEndIndex end index of range
  	 * @return true if there are any track points in this range
  	 */
  	public boolean isTrackPointWithin(int inStartIndex, int inEndIndex)
  	{
  		for (int i = inStartIndex; i <= inEndIndex; i++)
  		{
  			if (!getPoint(i).isWaypoint()) {
  				return true;
  			}
  		}
  		return false;
  	}
  
  	/**
  	 * @param inStartIndex start index of range
  	 * @param inEndIndex end index of range
  	 * @return true if there are any segment breaks in this range
  	 */
  	public boolean isSegmentBreakWithin(int inStartIndex, int inEndIndex)
  	{
  		for (int i = inStartIndex; i <= inEndIndex; i++)
  		{
  			DataPoint point = getPoint(i);
  			if (!point.isWaypoint() && point.getSegmentStart()) {
  				return true;
  			}
  		}
  		return false;
  	}
  
  
  	/**
  	 * Append the given point to the end of the track
  	 * @param inPoint point to append
  	 * @return true if successful
  	 */
  	public boolean appendPoint(DataPoint inPoint) {
  		return insertPoint(inPoint, _numPoints);
  	}
  
  	/**
  	 * Re-insert the specified point at the given index
  	 * @param inPoint point to insert
  	 * @param inIndex index at which to insert the point
  	 * @return true if it worked, false otherwise
  	 */
  	public boolean insertPoint(DataPoint inPoint, int inIndex)
  	{
  		if (inIndex > _numPoints || inPoint == null) {
  			return false;
  		}
  		// Make new array to copy points over to
  		DataPoint[] newPointArray = new DataPoint[_numPoints + 1];
  		if (inIndex > 0) {
  			System.arraycopy(_dataPoints, 0, newPointArray, 0, inIndex);
  		}
  		newPointArray[inIndex] = inPoint;
  		if (inIndex < _numPoints) {
  			System.arraycopy(_dataPoints, inIndex, newPointArray, inIndex+1, _numPoints - inIndex);
  		}
  		// Change over to new array
  		_dataPoints = newPointArray;
  		_numPoints++;
  		extendFieldList(inPoint.getFieldList());
  		// needs to be scaled again
  		_scaled = false;
  		UpdateMessageBroker.informSubscribers();
  		return true;
  	}
  
  	/**
  	 * Append the specified point range to the end of the track
  	 * @param inPoints list of points to append
  	 * @return true if it worked, false otherwise
  	 */
  	public boolean appendRange(List<DataPoint> inPoints)
  	{
  		if (inPoints == null || inPoints.isEmpty()) {
  			return false;
  		}
  		// Make new array to copy points over to
  		DataPoint[] newPointArray = new DataPoint[_numPoints + inPoints.size()];
  		System.arraycopy(_dataPoints, 0, newPointArray, 0, _numPoints);
  		int index = _numPoints;
  		for (DataPoint point : inPoints)
  		{
  			newPointArray[index] = point;
  			extendFieldList(point.getFieldList());
  			index++;
  		}
  		// Change over to new array
  		_dataPoints = newPointArray;
  		_numPoints = _dataPoints.length;
  		_scaled = false;
  		return true;
  	}
  }
  