1 package tim.prune.data;
3 import tim.prune.config.Config;
6 * Class to represent a single data point in the series
7 * including all its fields
8 * Can be either a track point or a waypoint
10 public class DataPoint
12 /** Array of Strings holding raw values */
13 private String[] _fieldValues = null;
14 /** list of field definitions */
15 private FieldList _fieldList = null;
16 /** Special fields for coordinates */
17 private Coordinate _latitude = null, _longitude = null;
18 private Altitude _altitude = null;
19 private Speed _hSpeed = null, _vSpeed = null;
20 private Timestamp _timestamp = null;
22 private Photo _photo = null;
23 /** Attached audio clip */
24 private AudioClip _audio = null;
25 private String _waypointName = null;
26 private boolean _startOfSegment = false;
27 private boolean _markedForDeletion = false;
28 private int _modifyCount = 0;
33 * @param inValueArray array of String values
34 * @param inFieldList list of fields
35 * @param inOptions creation options such as units
37 public DataPoint(String[] inValueArray, FieldList inFieldList, PointCreateOptions inOptions)
40 _fieldValues = inValueArray;
41 // save list of fields
42 _fieldList = inFieldList;
43 // Remove double quotes around values
44 removeQuotes(_fieldValues);
45 // parse fields into objects
46 parseFields(null, inOptions);
51 * Parse the string values into objects eg Coordinates
52 * @param inField field which has changed, or null for all
53 * @param inOptions creation options such as units
55 private void parseFields(Field inField, PointCreateOptions inOptions)
57 if (inOptions == null) inOptions = new PointCreateOptions();
58 if (inField == null || inField == Field.LATITUDE) {
59 _latitude = new Latitude(getFieldValue(Field.LATITUDE));
61 if (inField == null || inField == Field.LONGITUDE) {
62 _longitude = new Longitude(getFieldValue(Field.LONGITUDE));
64 if (inField == null || inField == Field.ALTITUDE)
66 Unit altUnit = inOptions.getAltitudeUnits();
67 if (_altitude != null && _altitude.getUnit() != null) {
68 altUnit = _altitude.getUnit();
70 _altitude = new Altitude(getFieldValue(Field.ALTITUDE), altUnit);
72 if (inField == null || inField == Field.SPEED)
74 _hSpeed = new Speed(getFieldValue(Field.SPEED), inOptions.getSpeedUnits());
76 if (inField == null || inField == Field.VERTICAL_SPEED)
78 _vSpeed = new Speed(getFieldValue(Field.VERTICAL_SPEED), inOptions.getVerticalSpeedUnits());
79 if (!inOptions.getVerticalSpeedsUpwards()) {
83 if (inField == null || inField == Field.TIMESTAMP) {
84 _timestamp = new Timestamp(getFieldValue(Field.TIMESTAMP));
86 if (inField == null || inField == Field.WAYPT_NAME) {
87 _waypointName = getFieldValue(Field.WAYPT_NAME);
89 if (inField == null || inField == Field.NEW_SEGMENT)
91 String segmentStr = getFieldValue(Field.NEW_SEGMENT);
92 if (segmentStr != null) {segmentStr = segmentStr.trim();}
93 _startOfSegment = (segmentStr != null && (segmentStr.equals("1") || segmentStr.toUpperCase().equals("Y")));
99 * Constructor for additional points (eg interpolated, photos)
100 * @param inLatitude latitude
101 * @param inLongitude longitude
102 * @param inAltitude altitude
104 public DataPoint(Coordinate inLatitude, Coordinate inLongitude, Altitude inAltitude)
106 // Only these three fields are available
107 _fieldValues = new String[3];
108 Field[] fields = {Field.LATITUDE, Field.LONGITUDE, Field.ALTITUDE};
109 _fieldList = new FieldList(fields);
110 _latitude = inLatitude;
111 _fieldValues[0] = inLatitude.output(Coordinate.FORMAT_NONE);
112 _longitude = inLongitude;
113 _fieldValues[1] = inLongitude.output(Coordinate.FORMAT_NONE);
114 if (inAltitude == null) {
115 _altitude = Altitude.NONE;
118 _altitude = inAltitude;
119 _fieldValues[2] = "" + inAltitude.getValue();
121 _timestamp = new Timestamp(null);
126 * Get the value for the given field
127 * @param inField field to interrogate
128 * @return value of field
130 public String getFieldValue(Field inField)
132 return getFieldValue(_fieldList.getFieldIndex(inField));
137 * Get the value at the given index
138 * @param inIndex index number starting at zero
139 * @return field value, or null if not found
141 private String getFieldValue(int inIndex)
143 if (_fieldValues == null || inIndex < 0 || inIndex >= _fieldValues.length)
145 return _fieldValues[inIndex];
150 * Set (or edit) the specified field value
151 * @param inField Field to set
152 * @param inValue value to set
153 * @param inUndo true if undo operation, false otherwise
155 public void setFieldValue(Field inField, String inValue, boolean inUndo)
157 // See if this data point already has this field
158 int fieldIndex = _fieldList.getFieldIndex(inField);
159 // Add to field list if necessary
162 // If value is empty & field doesn't exist then do nothing
163 if (inValue == null || inValue.equals(""))
167 // value isn't empty so extend field list
168 fieldIndex = _fieldList.extendList(inField);
170 // Extend array of field values if necessary
171 if (fieldIndex >= _fieldValues.length)
173 resizeValueArray(fieldIndex);
175 // Set field value in array
176 _fieldValues[fieldIndex] = inValue;
177 // Increment edit count on all field edits except segment
178 if (inField != Field.NEW_SEGMENT) {
181 // Change Coordinate, Altitude, Name or Timestamp fields after edit
182 if (_altitude != null && _altitude.getUnit() != null) {
183 // Altitude already present so reuse format
184 parseFields(inField, null); // current units will be used
187 // use default altitude format from config
188 parseFields(inField, Config.getUnitSet().getDefaultOptions());
193 * Either increment or decrement the modify count, depending on whether it's an undo or not
194 * @param inUndo true for undo, false otherwise
196 public void setModified(boolean inUndo)
207 * @return field list for this point
209 public FieldList getFieldList()
214 /** @param inFlag true for start of track segment */
215 public void setSegmentStart(boolean inFlag)
217 setFieldValue(Field.NEW_SEGMENT, inFlag?"1":null, false);
221 * Mark the point for deletion
222 * @param inFlag true to delete, false to keep
224 public void setMarkedForDeletion(boolean inFlag) {
225 _markedForDeletion = inFlag;
228 /** @return latitude */
229 public Coordinate getLatitude()
233 /** @return longitude */
234 public Coordinate getLongitude()
238 /** @return true if point has altitude */
239 public boolean hasAltitude()
241 return _altitude != null && _altitude.isValid();
243 /** @return altitude */
244 public Altitude getAltitude()
248 /** @return true if point has horizontal speed (loaded as field) */
249 public boolean hasHSpeed()
251 return _hSpeed != null && _hSpeed.isValid();
253 /** @return horizontal speed */
254 public Speed getHSpeed()
258 /** @return true if point has vertical speed (loaded as field) */
259 public boolean hasVSpeed()
261 return _vSpeed != null && _vSpeed.isValid();
263 /** @return vertical speed */
264 public Speed getVSpeed()
268 /** @return true if point has timestamp */
269 public boolean hasTimestamp()
271 return _timestamp.isValid();
273 /** @return timestamp */
274 public Timestamp getTimestamp()
278 /** @return waypoint name, if any */
279 public String getWaypointName()
281 return _waypointName;
284 /** @return true if start of new track segment */
285 public boolean getSegmentStart()
287 return _startOfSegment;
290 /** @return true if point marked for deletion */
291 public boolean getDeleteFlag()
293 return _markedForDeletion;
297 * @return true if point has a waypoint name
299 public boolean isWaypoint()
301 return (_waypointName != null && !_waypointName.equals(""));
305 * @return true if point has been modified since loading
307 public boolean isModified()
309 return _modifyCount > 0;
313 * Compare two DataPoint objects to see if they are duplicates
314 * @param inOther other object to compare
315 * @return true if the points are equivalent
317 public boolean isDuplicate(DataPoint inOther)
319 if (inOther == null) return false;
320 if (_longitude == null || _latitude == null
321 || inOther._longitude == null || inOther._latitude == null)
325 // Make sure photo points aren't specified as duplicates
326 if (_photo != null) return false;
327 // Compare latitude and longitude
328 if (!_longitude.equals(inOther._longitude) || !_latitude.equals(inOther._latitude))
332 // Note that conversion from decimal to dms can make non-identical points into duplicates
333 // Compare waypoint name (if any)
336 return !inOther.isWaypoint();
338 return (inOther._waypointName != null && inOther._waypointName.equals(_waypointName));
342 * Add an altitude offset to this point, and keep the point's string value in sync
343 * @param inOffset offset as double
344 * @param inUnit unit of offset, feet or metres
345 * @param inDecimals number of decimal places
347 public void addAltitudeOffset(double inOffset, Unit inUnit, int inDecimals)
351 _altitude.addOffset(inOffset, inUnit, inDecimals);
352 _fieldValues[_fieldList.getFieldIndex(Field.ALTITUDE)] = _altitude.getStringValue(null);
358 * Reset the altitude to the previous value (by an undo)
359 * @param inClone altitude object cloned from earlier
361 public void resetAltitude(Altitude inClone)
363 _altitude.reset(inClone);
364 _fieldValues[_fieldList.getFieldIndex(Field.ALTITUDE)] = _altitude.getStringValue(null);
369 * Add a time offset to this point
370 * @param inOffset offset to add (-ve to subtract)
372 public void addTimeOffset(long inOffset)
376 _timestamp.addOffset(inOffset);
377 _fieldValues[_fieldList.getFieldIndex(Field.TIMESTAMP)] = _timestamp.getText();
383 * Set the photo for this data point
384 * @param inPhoto Photo object
386 public void setPhoto(Photo inPhoto) {
392 * @return associated Photo object
394 public Photo getPhoto() {
399 * Set the audio clip for this point
400 * @param inAudio audio object
402 public void setAudio(AudioClip inAudio) {
408 * @return associated audio object
410 public AudioClip getAudio() {
415 * Attach the given media object according to type
416 * @param inMedia either a photo or an audio clip
418 public void attachMedia(MediaObject inMedia)
420 if (inMedia != null) {
421 if (inMedia instanceof Photo) {
422 setPhoto((Photo) inMedia);
423 inMedia.setDataPoint(this);
425 else if (inMedia instanceof AudioClip) {
426 setAudio((AudioClip) inMedia);
427 inMedia.setDataPoint(this);
433 * @return true if the point is valid
435 public boolean isValid()
437 return _latitude.isValid() && _longitude.isValid();
441 * @return true if the point has either a photo or audio attached
443 public boolean hasMedia() {
444 return _photo != null || _audio != null;
448 * @return name of attached photo and/or audio
450 public String getMediaName()
452 String mediaName = null;
453 if (_photo != null) mediaName = _photo.getName();
456 if (mediaName == null) {
457 mediaName = _audio.getName();
460 mediaName = mediaName + ", " + _audio.getName();
467 * Interpolate a set of points between this one and the given one
468 * @param inEndPoint end point of interpolation
469 * @param inNumPoints number of points to generate
470 * @return the DataPoint array
472 public DataPoint[] interpolate(DataPoint inEndPoint, int inNumPoints)
474 DataPoint[] range = new DataPoint[inNumPoints];
476 for (int i=0; i<inNumPoints; i++)
478 Coordinate latitude = Coordinate.interpolate(_latitude, inEndPoint.getLatitude(), i, inNumPoints);
479 Coordinate longitude = Coordinate.interpolate(_longitude, inEndPoint.getLongitude(), i, inNumPoints);
480 Altitude altitude = Altitude.interpolate(_altitude, inEndPoint.getAltitude(), i, inNumPoints);
481 range[i] = new DataPoint(latitude, longitude, altitude);
487 * Interpolate between the two given points
488 * @param inStartPoint start point
489 * @param inEndPoint end point
490 * @param inFrac fractional distance from first point (0.0 to 1.0)
491 * @return new DataPoint object between two given ones
493 public static DataPoint interpolate(DataPoint inStartPoint, DataPoint inEndPoint, double inFrac)
495 if (inStartPoint == null || inEndPoint == null) {return null;}
496 return new DataPoint(
497 Coordinate.interpolate(inStartPoint.getLatitude(), inEndPoint.getLatitude(), inFrac),
498 Coordinate.interpolate(inStartPoint.getLongitude(), inEndPoint.getLongitude(), inFrac),
499 Altitude.interpolate(inStartPoint.getAltitude(), inEndPoint.getAltitude(), inFrac)
504 * Calculate the number of radians between two points (for distance calculation)
505 * @param inPoint1 first point
506 * @param inPoint2 second point
507 * @return angular distance between points in radians
509 public static double calculateRadiansBetween(DataPoint inPoint1, DataPoint inPoint2)
511 if (inPoint1 == null || inPoint2 == null)
513 final double TO_RADIANS = Math.PI / 180.0;
514 // Get lat and long from points
515 double lat1 = inPoint1.getLatitude().getDouble() * TO_RADIANS;
516 double lat2 = inPoint2.getLatitude().getDouble() * TO_RADIANS;
517 double lon1 = inPoint1.getLongitude().getDouble() * TO_RADIANS;
518 double lon2 = inPoint2.getLongitude().getDouble() * TO_RADIANS;
519 // Formula given by Wikipedia:Great-circle_distance as follows:
520 // angle = 2 arcsin( sqrt( (sin ((lat2-lat1)/2))^^2 + cos(lat1)cos(lat2)(sin((lon2-lon1)/2))^^2))
521 double firstSine = Math.sin((lat2-lat1) / 2.0);
522 double secondSine = Math.sin((lon2-lon1) / 2.0);
523 double term2 = Math.cos(lat1) * Math.cos(lat2) * secondSine * secondSine;
524 double answer = 2 * Math.asin(Math.sqrt(firstSine*firstSine + term2));
531 * Resize the value array
532 * @param inNewIndex new index to allow
534 private void resizeValueArray(int inNewIndex)
536 int newSize = inNewIndex + 1;
537 if (newSize > _fieldValues.length)
539 String[] newArray = new String[newSize];
540 System.arraycopy(_fieldValues, 0, newArray, 0, _fieldValues.length);
541 _fieldValues = newArray;
547 * @return a clone object with copied data
549 public DataPoint clonePoint()
551 // Copy all values (note that photo not copied)
552 String[] valuesCopy = new String[_fieldValues.length];
553 System.arraycopy(_fieldValues, 0, valuesCopy, 0, _fieldValues.length);
555 PointCreateOptions options = new PointCreateOptions();
556 if (_altitude != null) {
557 options.setAltitudeUnits(_altitude.getUnit());
559 // Make new object to hold cloned data
560 DataPoint point = new DataPoint(valuesCopy, _fieldList, options);
561 // Copy the speed information
563 point.getHSpeed().copyFrom(_hSpeed);
566 point.getVSpeed().copyFrom(_vSpeed);
573 * Remove all single and double quotes surrounding each value
574 * @param inValues array of values
576 private static void removeQuotes(String[] inValues)
578 if (inValues == null) {return;}
579 for (int i=0; i<inValues.length; i++)
581 inValues[i] = removeQuotes(inValues[i]);
586 * Remove any single or double quotes surrounding a value
587 * @param inValue value to modify
588 * @return modified String
590 private static String removeQuotes(String inValue)
592 if (inValue == null) {return inValue;}
593 final int len = inValue.length();
594 if (len <= 1) {return inValue;}
595 // get the first and last characters
596 final char firstChar = inValue.charAt(0);
597 final char lastChar = inValue.charAt(len-1);
598 if (firstChar == lastChar)
600 if (firstChar == '\"' || firstChar == '\'') {
601 return inValue.substring(1, len-1);
608 * Get string for debug
609 * @see java.lang.Object#toString()
611 public String toString()
613 return "[Lat=" + getLatitude().toString() + ", Lon=" + getLongitude().toString() + "]";