content: package tim.prune.data;
  
  /**
   * Class to represent a single data point in the series
   * including all its fields
   * Can be either a track point or a waypoint
   */
  public class DataPoint
  {
  	/** Array of Strings holding raw values */
  	private String[] _fieldValues;
  	/** list of field definitions */
  	private final FieldList _fieldList;
  	/** Special fields for coordinates */
  	private Coordinate _latitude = null, _longitude = null;
  	private Altitude _altitude = null;
  	private Speed _hSpeed = null, _vSpeed = null;
  	private Timestamp _timestamp = null;
  	private SourceInfo _sourceInfo = null;
  	private int _originalIndex = -1;
  	/** Attached photo */
  	private Photo _photo = null;
  	/** Attached audio clip */
  	private AudioClip _audio = null;
  	private String _waypointName = null;
  	private boolean _startOfSegment = false;
  	private int _modifyCount = 0;
  
  	private static FieldList _sharedLatLonAltFieldList = null;
  
  
  	/**
  	 * Constructor
  	 * @param inValueArray array of String values
  	 * @param inFieldList list of fields
  	 * @param inOptions creation options such as units
  	 */
  	public DataPoint(String[] inValueArray, FieldList inFieldList, PointCreateOptions inOptions)
  	{
  		// save data
  		_fieldValues = inValueArray;
  		// save list of fields
  		_fieldList = inFieldList;
  		// Remove double quotes around values
  		removeQuotes(_fieldValues);
  		// parse fields into objects
  		parseFields(null, inOptions);
  	}
  
  
  	/**
  	 * Parse the string values into objects eg Coordinates
  	 * @param inField field which has changed, or null for all
  	 * @param inOptions creation options such as units
  	 */
  	private void parseFields(Field inField, PointCreateOptions inOptions)
  	{
  		if (inOptions == null) inOptions = new PointCreateOptions();
  		if (inField == null || inField == Field.LATITUDE) {
  			_latitude = Latitude.make(getFieldValue(Field.LATITUDE));
  		}
  		if (inField == null || inField == Field.LONGITUDE) {
  			_longitude = Longitude.make(getFieldValue(Field.LONGITUDE));
  		}
  		if (inField == null || inField == Field.ALTITUDE)
  		{
  			final Unit altUnit;
  			if (_altitude != null && _altitude.getUnit() != null) {
  				altUnit = _altitude.getUnit();
  			}
  			else {
  				altUnit = inOptions.getAltitudeUnits();
  			}
  			_altitude = new Altitude(getFieldValue(Field.ALTITUDE), altUnit);
  		}
  		if (inField == null || inField == Field.SPEED)
  		{
  			final Unit speedUnit;
  			if (_hSpeed != null && _hSpeed.getUnit() != null) {
  				speedUnit = _hSpeed.getUnit();
  			}
  			else {
  				speedUnit = inOptions.getSpeedUnits();
  			}
  			_hSpeed = Speed.createOrNull(getFieldValue(Field.SPEED), speedUnit);
  		}
  		if (inField == null || inField == Field.VERTICAL_SPEED)
  		{
  			final Unit vspeedUnit;
  			final boolean isInverted;
  			if (_vSpeed != null && _vSpeed.getUnit() != null)
  			{
  				vspeedUnit = _vSpeed.getUnit();
  				isInverted = _vSpeed.isInverted();
  			}
  			else
  			{
  				vspeedUnit = inOptions.getVerticalSpeedUnits();
  				isInverted = !inOptions.getVerticalSpeedsUpwards();
  			}
  			_vSpeed = Speed.createOrNull(getFieldValue(Field.VERTICAL_SPEED), vspeedUnit, isInverted);
  		}
  		if (inField == null || inField == Field.TIMESTAMP) {
  			_timestamp = new TimestampUtc(getFieldValue(Field.TIMESTAMP));
  		}
  		if (inField == null || inField == Field.WAYPT_NAME) {
  			_waypointName = getFieldValue(Field.WAYPT_NAME);
  		}
  		if (inField == null || inField == Field.NEW_SEGMENT)
  		{
  			String segmentStr = getFieldValue(Field.NEW_SEGMENT);
  			if (segmentStr != null) {segmentStr = segmentStr.trim();}
  			_startOfSegment = (segmentStr != null && (segmentStr.equals("1") || segmentStr.equalsIgnoreCase("Y")));
  		}
  	}
  
  	/**
  	 * Constructor for additional points without altitude
  	 * @param inLatitude latitude
  	 * @param inLongitude longitude
  	 */
  	public DataPoint(Coordinate inLatitude, Coordinate inLongitude) {
  		this(inLatitude, inLongitude, null);
  	}
  
  	/**
  	 * Constructor for additional points (eg interpolated, photos)
  	 * @param inLatitude latitude
  	 * @param inLongitude longitude
  	 * @param inAltitude altitude
  	 */
  	public DataPoint(Coordinate inLatitude, Coordinate inLongitude, Altitude inAltitude)
  	{
  		// Only these three fields are available
  		_fieldValues = new String[3];
  		_fieldList = getSharedLatLonFieldList();
  		// TODO: Check if either latitude or longitude is null - in which case do what?
  		_latitude = inLatitude;
  		_fieldValues[0] = inLatitude.toString();
  		_longitude = inLongitude;
  		_fieldValues[1] = inLongitude.toString();
  		if (inAltitude == null) {
  			_altitude = Altitude.NONE;
  		}
  		else {
  			_altitude = inAltitude;
  			_fieldValues[2] = "" + inAltitude.getValue();
  		}
  		_timestamp = new TimestampUtc(null);
  	}
  
  	/** Simplified constructor just giving raw values for latitude and longitude */
  	public DataPoint(double inLatitude, double inLongitude) {
  		this(Latitude.make(inLatitude), Longitude.make(inLongitude), null);
  	}
  
  	/** @return a shared field list just containing latitude longitude and altitude */
  	private static FieldList getSharedLatLonFieldList()
  	{
  		if (_sharedLatLonAltFieldList == null)
  		{
  			Field[] fields = {Field.LATITUDE, Field.LONGITUDE, Field.ALTITUDE};
  			_sharedLatLonAltFieldList = new FieldList(fields);
  		}
  		return _sharedLatLonAltFieldList;
  	}
  
  	/**
  	 * Get the value for the given field
  	 * @param inField field to interrogate
  	 * @return value of field
  	 */
  	public String getFieldValue(Field inField) {
  		return getFieldValue(_fieldList.getFieldIndex(inField));
  	}
  
  	/**
  	 * Get the value at the given index
  	 * @param inIndex index number starting at zero
  	 * @return field value, or null if not found
  	 */
  	private String getFieldValue(int inIndex)
  	{
  		if (_fieldValues == null || inIndex < 0 || inIndex >= _fieldValues.length) {
  			return null;
  		}
  		return _fieldValues[inIndex];
  	}
  
  	/**
  	 * Set (or edit) the waypoint name field
  	 * @param inValue name to set
  	 */
  	public void setWaypointName(String inValue)
  	{
  		setRawFieldValue(Field.WAYPT_NAME, inValue);
  		setModified(false);
  		_waypointName = inValue;
  	}
  
  	/**
  	 * Set (or edit) the specified field value, without specifying a unit set
  	 * @param inField Field to set
  	 * @param inValue value to set
  	 * @param inUndo true if undo operation, false otherwise
  	 */
  	public void setFieldValue(Field inField, String inValue, boolean inUndo) {
  		setFieldValue(inField, inValue, null, inUndo);
  	}
  
  	/**
  	 * Set (or edit) the specified field value
  	 * @param inField Field to set
  	 * @param inValue value to set
  	 * @param inUnitSet unit set to use
  	 * @param inUndo true if undo operation, false otherwise
  	 */
  	public void setFieldValue(Field inField, String inValue, UnitSet inUnitSet, boolean inUndo)
  	{
  		setRawFieldValue(inField, inValue);
  		// Increment edit count on all field edits except segment
  		if (inField != Field.NEW_SEGMENT) {
  			setModified(inUndo);
  		}
  
  		final boolean needUnits;
  		if (inField == Field.ALTITUDE) {
  			needUnits = _altitude == null || _altitude.getUnit() == null;
  		}
  		else if (inField == Field.SPEED) {
  			needUnits = _hSpeed == null || _hSpeed.getUnit() == null;
  		}
  		else if (inField == Field.VERTICAL_SPEED) {
  			needUnits = _vSpeed == null || _vSpeed.getUnit() == null;
  		}
  		else {
  			needUnits = false;
  		}
  		if (needUnits && inUnitSet == null && inValue != null && !inValue.equals("")) {
  			throw new IllegalArgumentException("Units required to set field " + inField.getName());
  		}
  
  		parseFields(inField, inUnitSet == null ? null : inUnitSet.getDefaultOptions());
  	}
  
  	/**
  	 * Extend the field list if necessary and set the raw field value
  	 * @param inField field to set
  	 * @param inValue value to set
  	 */
  	private void setRawFieldValue(Field inField, String inValue)
  	{
  		if (inField == null) {
  			return;
  		}
  		// See if this data point already has this field
  		int fieldIndex = _fieldList.getFieldIndex(inField);
  		// Add to field list if necessary
  		if (fieldIndex < 0)
  		{
  			// If value is empty & field doesn't exist then do nothing
  			if (inValue == null || inValue.equals("")) {
  				return;
  			}
  			// value isn't empty so extend field list
  			fieldIndex = _fieldList.addField(inField);
  		}
  		// Extend array of field values if necessary
  		if (fieldIndex >= _fieldValues.length) {
  			resizeValueArray(fieldIndex);
  		}
  		// Set field value in array
  		_fieldValues[fieldIndex] = inValue;
  	}
  
  	/**
  	 * Either increment or decrement the modify count, depending on whether it's an undo or not
  	 * @param inUndo true for undo, false otherwise
  	 */
  	public void setModified(boolean inUndo)
  	{
  		if (!inUndo) {
  			_modifyCount++;
  		}
  		else {
  			_modifyCount--;
  		}
  	}
  
  	/**
  	 * @return field list for this point
  	 */
  	public FieldList getFieldList() {
  		return _fieldList;
  	}
  
  	/** @param inFlag true for start of track segment */
  	public void setSegmentStart(boolean inFlag) {
  		setFieldValue(Field.NEW_SEGMENT, inFlag ? "1" : null, false);
  	}
  
  	/** @return latitude */
  	public Coordinate getLatitude() {
  		return _latitude;
  	}
  
  	/** @return longitude */
  	public Coordinate getLongitude() {
  		return _longitude;
  	}
  
  	/** @return true if point has altitude */
  	public boolean hasAltitude() {
  		return _altitude != null && _altitude.isValid();
  	}
  
  	/** @return altitude */
  	public Altitude getAltitude() {
  		return _altitude;
  	}
  
  	/** @return true if point has horizontal speed (loaded as field) */
  	public boolean hasHSpeed() {
  		return _hSpeed != null && _hSpeed.isValid();
  	}
  
  	/** @return horizontal speed */
  	public Speed getHSpeed() {
  		return _hSpeed;
  	}
  
  	/** @return true if point has vertical speed (loaded as field) */
  	public boolean hasVSpeed() {
  		return _vSpeed != null && _vSpeed.isValid();
  	}
  
  	/** @return vertical speed */
  	public Speed getVSpeed() {
  		return _vSpeed;
  	}
  
  	/** @return true if point has timestamp */
  	public boolean hasTimestamp() {
  		return _timestamp != null && _timestamp.isValid();
  	}
  
  	/** @return timestamp */
  	public Timestamp getTimestamp() {
  		return _timestamp;
  	}
  
  	/** @return waypoint name, if any */
  	public String getWaypointName() {
  		return _waypointName;
  	}
  
  	/** @return true if start of new track segment */
  	public boolean getSegmentStart() {
  		return _startOfSegment;
  	}
  
  	/**
  	 * @return true if point has a waypoint name
  	 */
  	public boolean isWaypoint() {
  		return _waypointName != null && !_waypointName.equals("");
  	}
  
  	/**
  	 * @return true if point has been modified since loading
  	 */
  	public boolean isModified() {
  		return _modifyCount > 0;
  	}
  
  	/**
  	 * Compare two DataPoint objects to see if they are duplicates
  	 * @param inOther other object to compare
  	 * @return true if the points are equivalent
  	 */
  	public boolean isDuplicate(DataPoint inOther)
  	{
  		if (inOther == null) return false;
  		if (_longitude == null || _latitude == null
  			|| inOther._longitude == null || inOther._latitude == null)
  		{
  			return false;
  		}
  		// Make sure photo points aren't specified as duplicates
  		if (_photo != null) return false;
  		// Compare latitude and longitude
  		if (!_longitude.equals(inOther._longitude) || !_latitude.equals(inOther._latitude))
  		{
  			return false;
  		}
  		// Note that conversion from decimal to dms can make non-identical points into duplicates
  		// Compare waypoint name (if any)
  		if (!isWaypoint()) {
  			return !inOther.isWaypoint();
  		}
  		return (inOther._waypointName != null && inOther._waypointName.equals(_waypointName));
  	}
  
  	/**
  	 * Set the altitude including units
  	 * @param inValue value as string
  	 * @param inUnit units
  	 * @param inIsUndo true if it's an undo operation
  	 */
  	public void setAltitude(String inValue, Unit inUnit, boolean inIsUndo)
  	{
  		_altitude = new Altitude(inValue, inUnit);
  		setFieldValue(Field.ALTITUDE, inValue, inIsUndo);
  		setModified(inIsUndo);
  	}
  
  	/**
  	 * Set the altitude from another altitude value
  	 */
  	public void setAltitude(Altitude inAltitude)
  	{
  		_altitude = new Altitude(inAltitude);
  		setFieldValue(Field.ALTITUDE, _altitude.getStringValue(null), false);
  		setModified(false);
  	}
  
  	/**
  	 * Set the photo for this data point
  	 * @param inPhoto Photo object
  	 */
  	public void setPhoto(Photo inPhoto) {
  		_photo = inPhoto;
  		_modifyCount++;
  	}
  
  	/**
  	 * @return associated Photo object
  	 */
  	public Photo getPhoto() {
  		return _photo;
  	}
  
  	/**
  	 * Set the audio clip for this point
  	 * @param inAudio audio object
  	 */
  	public void setAudio(AudioClip inAudio) {
  		_audio = inAudio;
  		_modifyCount++;
  	}
  
  	/**
  	 * @return associated audio object
  	 */
  	public AudioClip getAudio() {
  		return _audio;
  	}
  
  	/**
  	 * @return true if the point is valid
  	 */
  	public boolean isValid() {
  		return _latitude != null && _longitude != null;
  	}
  
  	/**
  	 * @return true if the point has either a photo or audio attached
  	 */
  	public boolean hasMedia() {
  		return _photo != null || _audio != null;
  	}
  
  	/**
  	 * @return name of attached photo and/or audio
  	 */
  	public String getMediaName()
  	{
  		String mediaName = null;
  		if (_photo != null) {
  			mediaName = _photo.getName();
  		}
  		if (_audio != null)
  		{
  			if (mediaName == null) {
  				mediaName = _audio.getName();
  			}
  			else {
  				mediaName = mediaName + ", " + _audio.getName();
  			}
  		}
  		return mediaName;
  	}
  
  	/**
  	 * Calculate the number of radians between two points (for distance calculation)
  	 * @param inPoint1 first point
  	 * @param inPoint2 second point
  	 * @return angular distance between points in radians
  	 */
  	public static double calculateRadiansBetween(DataPoint inPoint1, DataPoint inPoint2)
  	{
  		if (inPoint1 == null || inPoint2 == null) {
  			return 0.0;
  		}
  		// Get lat and long from points, in degrees
  		double lat1 = inPoint1.getLatitude().getDouble();
  		double lat2 = inPoint2.getLatitude().getDouble();
  		double lon1 = inPoint1.getLongitude().getDouble();
  		double lon2 = inPoint2.getLongitude().getDouble();
  		return Distance.calculateRadiansBetween(lat1, lon1, lat2, lon2);
  	}
  
  
  	/**
  	 * Resize the value array
  	 * @param inNewIndex new index to allow
  	 */
  	private void resizeValueArray(int inNewIndex)
  	{
  		int newSize = inNewIndex + 1;
  		if (newSize > _fieldValues.length)
  		{
  			String[] newArray = new String[newSize];
  			System.arraycopy(_fieldValues, 0, newArray, 0, _fieldValues.length);
  			_fieldValues = newArray;
  		}
  	}
  
  
  	/**
  	 * @return a clone object with copied data
  	 */
  	public DataPoint clonePoint()
  	{
  		// Copy all values (note that photo is not copied)
  		String[] valuesCopy = new String[_fieldValues.length];
  		System.arraycopy(_fieldValues, 0, valuesCopy, 0, _fieldValues.length);
  
  		PointCreateOptions options = new PointCreateOptions();
  		if (_altitude != null) {
  			options.setAltitudeUnits(_altitude.getUnit());
  		}
  		// Make new object to hold cloned data
  		DataPoint point = new DataPoint(valuesCopy, _fieldList, options);
  		// Copy the speed information
  		if (hasHSpeed()) {
  			point.getHSpeed().copyFrom(_hSpeed);
  		}
  		if (hasVSpeed()) {
  			point.getVSpeed().copyFrom(_vSpeed);
  		}
  		return point;
  	}
  
  	public void setSourceInfo(SourceInfo inInfo) {
  		_sourceInfo = inInfo;
  	}
  
  	public SourceInfo getSourceInfo() {
  		return _sourceInfo;
  	}
  
  	public void setOriginalIndex(int inIndex) {
  		_originalIndex = inIndex;
  	}
  
  	public int getOriginalIndex() {
  		return _originalIndex;
  	}
  
  	/**
  	 * Remove all single and double quotes surrounding each value
  	 * @param inValues array of values
  	 */
  	private static void removeQuotes(String[] inValues)
  	{
  		if (inValues != null)
  		{
  			for (int i=0; i<inValues.length; i++) {
  				inValues[i] = removeQuotes(inValues[i]);
  			}
  		}
  	}
  
  	/**
  	 * Remove any single or double quotes surrounding a value
  	 * @param inValue value to modify
  	 * @return modified String
  	 */
  	private static String removeQuotes(String inValue)
  	{
  		if (inValue == null) {
  			return null;
  		}
  		final int len = inValue.length();
  		if (len <= 1) {return inValue;}
  		// get the first and last characters
  		final char firstChar = inValue.charAt(0);
  		final char lastChar  = inValue.charAt(len-1);
  		if (firstChar == lastChar)
  		{
  			if (firstChar == '\"' || firstChar == '\'') {
  				return inValue.substring(1, len-1);
  			}
  		}
  		return inValue;
  	}
  
  	/**
  	 * Get string for debug
  	 */
  	public String toString() {
  		return "[Lat=" + getLatitude().toString() + ", Lon=" + getLongitude().toString() + "]";
  	}
  }
  