--- /dev/null
+package tim.prune.data;
+
+import tim.prune.config.Config;
+
+/**
+ * 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 = null;
+ /** list of field definitions */
+ private FieldList _fieldList = null;
+ /** Special fields for coordinates */
+ private Coordinate _latitude = null, _longitude = null;
+ private Altitude _altitude = null;
+ private Speed _hSpeed = null, _vSpeed = null;
+ private Timestamp _timestamp = null;
+ /** Attached photo */
+ private Photo _photo = null;
+ /** Attached audio clip */
+ private AudioClip _audio = null;
+ private String _waypointName = null;
+ private boolean _startOfSegment = false;
+ private boolean _markedForDeletion = false;
+ private int _modifyCount = 0;
+
+
+ /**
+ * 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 = new Latitude(getFieldValue(Field.LATITUDE));
+ }
+ if (inField == null || inField == Field.LONGITUDE) {
+ _longitude = new Longitude(getFieldValue(Field.LONGITUDE));
+ }
+ if (inField == null || inField == Field.ALTITUDE)
+ {
+ Unit altUnit = inOptions.getAltitudeUnits();
+ if (_altitude != null && _altitude.getUnit() != null) {
+ altUnit = _altitude.getUnit();
+ }
+ _altitude = new Altitude(getFieldValue(Field.ALTITUDE), altUnit);
+ }
+ if (inField == null || inField == Field.SPEED)
+ {
+ _hSpeed = new Speed(getFieldValue(Field.SPEED), inOptions.getSpeedUnits());
+ }
+ if (inField == null || inField == Field.VERTICAL_SPEED)
+ {
+ _vSpeed = new Speed(getFieldValue(Field.VERTICAL_SPEED), inOptions.getVerticalSpeedUnits());
+ if (!inOptions.getVerticalSpeedsUpwards()) {
+ _vSpeed.invert();
+ }
+ }
+ 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.toUpperCase().equals("Y")));
+ }
+ }
+
+
+ /**
+ * 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];
+ Field[] fields = {Field.LATITUDE, Field.LONGITUDE, Field.ALTITUDE};
+ _fieldList = new FieldList(fields);
+ _latitude = inLatitude;
+ _fieldValues[0] = inLatitude.output(Coordinate.FORMAT_NONE);
+ _longitude = inLongitude;
+ _fieldValues[1] = inLongitude.output(Coordinate.FORMAT_NONE);
+ if (inAltitude == null) {
+ _altitude = Altitude.NONE;
+ }
+ else {
+ _altitude = inAltitude;
+ _fieldValues[2] = "" + inAltitude.getValue();
+ }
+ _timestamp = new TimestampUtc(null);
+ }
+
+
+ /**
+ * 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 specified field value
+ * @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)
+ {
+ // 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.extendList(inField);
+ }
+ // Extend array of field values if necessary
+ if (fieldIndex >= _fieldValues.length)
+ {
+ resizeValueArray(fieldIndex);
+ }
+ // Set field value in array
+ _fieldValues[fieldIndex] = inValue;
+ // Increment edit count on all field edits except segment
+ if (inField != Field.NEW_SEGMENT) {
+ setModified(inUndo);
+ }
+ // Change Coordinate, Altitude, Name or Timestamp fields after edit
+ if (_altitude != null && _altitude.getUnit() != null) {
+ // Altitude already present so reuse format
+ parseFields(inField, null); // current units will be used
+ }
+ else {
+ // use default altitude format from config
+ parseFields(inField, Config.getUnitSet().getDefaultOptions());
+ }
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Mark the point for deletion
+ * @param inFlag true to delete, false to keep
+ */
+ public void setMarkedForDeletion(boolean inFlag) {
+ _markedForDeletion = inFlag;
+ }
+
+ /** @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.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 marked for deletion */
+ public boolean getDeleteFlag()
+ {
+ return _markedForDeletion;
+ }
+
+ /**
+ * @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));
+ }
+
+ /**
+ * Add an altitude offset to this point, and keep the point's string value in sync
+ * @param inOffset offset as double
+ * @param inUnit unit of offset, feet or metres
+ * @param inDecimals number of decimal places
+ */
+ public void addAltitudeOffset(double inOffset, Unit inUnit, int inDecimals)
+ {
+ if (hasAltitude())
+ {
+ _altitude.addOffset(inOffset, inUnit, inDecimals);
+ _fieldValues[_fieldList.getFieldIndex(Field.ALTITUDE)] = _altitude.getStringValue(null);
+ setModified(false);
+ }
+ }
+
+ /**
+ * Reset the altitude to the previous value (by an undo)
+ * @param inClone altitude object cloned from earlier
+ */
+ public void resetAltitude(Altitude inClone)
+ {
+ _altitude.reset(inClone);
+ _fieldValues[_fieldList.getFieldIndex(Field.ALTITUDE)] = _altitude.getStringValue(null);
+ setModified(true);
+ }
+
+ /**
+ * Add a time offset to this point
+ * @param inOffset offset to add (-ve to subtract)
+ */
+ public void addTimeOffsetSeconds(long inOffset)
+ {
+ if (hasTimestamp())
+ {
+ _timestamp.addOffsetSeconds(inOffset);
+ _fieldValues[_fieldList.getFieldIndex(Field.TIMESTAMP)] = _timestamp.getText(null);
+ 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;
+ }
+
+ /**
+ * Attach the given media object according to type
+ * @param inMedia either a photo or an audio clip
+ */
+ public void attachMedia(MediaObject inMedia)
+ {
+ if (inMedia != null) {
+ if (inMedia instanceof Photo) {
+ setPhoto((Photo) inMedia);
+ inMedia.setDataPoint(this);
+ }
+ else if (inMedia instanceof AudioClip) {
+ setAudio((AudioClip) inMedia);
+ inMedia.setDataPoint(this);
+ }
+ }
+ }
+
+ /**
+ * @return true if the point is valid
+ */
+ public boolean isValid()
+ {
+ return _latitude.isValid() && _longitude.isValid();
+ }
+
+ /**
+ * @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;
+ }
+
+ /**
+ * Interpolate a set of points between this one and the given one
+ * @param inEndPoint end point of interpolation
+ * @param inNumPoints number of points to generate
+ * @return the DataPoint array
+ */
+ public DataPoint[] interpolate(DataPoint inEndPoint, int inNumPoints)
+ {
+ DataPoint[] range = new DataPoint[inNumPoints];
+ // Loop over points
+ for (int i=0; i<inNumPoints; i++)
+ {
+ Coordinate latitude = Coordinate.interpolate(_latitude, inEndPoint.getLatitude(), i, inNumPoints);
+ Coordinate longitude = Coordinate.interpolate(_longitude, inEndPoint.getLongitude(), i, inNumPoints);
+ Altitude altitude = Altitude.interpolate(_altitude, inEndPoint.getAltitude(), i, inNumPoints);
+ range[i] = new DataPoint(latitude, longitude, altitude);
+ }
+ return range;
+ }
+
+ /**
+ * Interpolate between the two given points
+ * @param inStartPoint start point
+ * @param inEndPoint end point
+ * @param inFrac fractional distance from first point (0.0 to 1.0)
+ * @return new DataPoint object between two given ones
+ */
+ public static DataPoint interpolate(DataPoint inStartPoint, DataPoint inEndPoint, double inFrac)
+ {
+ if (inStartPoint == null || inEndPoint == null) {return null;}
+ return new DataPoint(
+ Coordinate.interpolate(inStartPoint.getLatitude(), inEndPoint.getLatitude(), inFrac),
+ Coordinate.interpolate(inStartPoint.getLongitude(), inEndPoint.getLongitude(), inFrac),
+ Altitude.interpolate(inStartPoint.getAltitude(), inEndPoint.getAltitude(), inFrac)
+ );
+ }
+
+ /**
+ * 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;
+ final double TO_RADIANS = Math.PI / 180.0;
+ // Get lat and long from points
+ double lat1 = inPoint1.getLatitude().getDouble() * TO_RADIANS;
+ double lat2 = inPoint2.getLatitude().getDouble() * TO_RADIANS;
+ double lon1 = inPoint1.getLongitude().getDouble() * TO_RADIANS;
+ double lon2 = inPoint2.getLongitude().getDouble() * TO_RADIANS;
+ // Formula given by Wikipedia:Great-circle_distance as follows:
+ // angle = 2 arcsin( sqrt( (sin ((lat2-lat1)/2))^^2 + cos(lat1)cos(lat2)(sin((lon2-lon1)/2))^^2))
+ double firstSine = Math.sin((lat2-lat1) / 2.0);
+ double secondSine = Math.sin((lon2-lon1) / 2.0);
+ double term2 = Math.cos(lat1) * Math.cos(lat2) * secondSine * secondSine;
+ double answer = 2 * Math.asin(Math.sqrt(firstSine*firstSine + term2));
+ // phew
+ return answer;
+ }
+
+
+ /**
+ * 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 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;
+ }
+
+
+ /**
+ * Remove all single and double quotes surrounding each value
+ * @param inValues array of values
+ */
+ private static void removeQuotes(String[] inValues)
+ {
+ if (inValues == null) {return;}
+ 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 inValue;}
+ 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
+ * @see java.lang.Object#toString()
+ */
+ public String toString()
+ {
+ return "[Lat=" + getLatitude().toString() + ", Lon=" + getLongitude().toString() + "]";
+ }
+}