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 TimestampUtc(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 TimestampUtc(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 * Remove altitude from point
360 public void removeAltitude()
362 _altitude = Altitude.NONE;
363 _fieldValues[_fieldList.getFieldIndex(Field.ALTITUDE)] = _altitude.getStringValue(null);
368 * Reset the altitude to the previous value (by an undo)
369 * @param inClone altitude object cloned from earlier
371 public void resetAltitude(Altitude inClone)
373 _altitude.reset(inClone);
374 _fieldValues[_fieldList.getFieldIndex(Field.ALTITUDE)] = _altitude.getStringValue(null);
379 * Add a time offset to this point
380 * @param inOffset offset to add (-ve to subtract)
382 public void addTimeOffsetSeconds(long inOffset)
386 _timestamp.addOffsetSeconds(inOffset);
387 _fieldValues[_fieldList.getFieldIndex(Field.TIMESTAMP)] = _timestamp.getText(null);
393 * Set the photo for this data point
394 * @param inPhoto Photo object
396 public void setPhoto(Photo inPhoto) {
402 * @return associated Photo object
404 public Photo getPhoto() {
409 * Set the audio clip for this point
410 * @param inAudio audio object
412 public void setAudio(AudioClip inAudio) {
418 * @return associated audio object
420 public AudioClip getAudio() {
425 * Attach the given media object according to type
426 * @param inMedia either a photo or an audio clip
428 public void attachMedia(MediaObject inMedia)
430 if (inMedia != null) {
431 if (inMedia instanceof Photo) {
432 setPhoto((Photo) inMedia);
433 inMedia.setDataPoint(this);
435 else if (inMedia instanceof AudioClip) {
436 setAudio((AudioClip) inMedia);
437 inMedia.setDataPoint(this);
443 * @return true if the point is valid
445 public boolean isValid()
447 return _latitude.isValid() && _longitude.isValid();
451 * @return true if the point has either a photo or audio attached
453 public boolean hasMedia() {
454 return _photo != null || _audio != null;
458 * @return name of attached photo and/or audio
460 public String getMediaName()
462 String mediaName = null;
463 if (_photo != null) mediaName = _photo.getName();
466 if (mediaName == null) {
467 mediaName = _audio.getName();
470 mediaName = mediaName + ", " + _audio.getName();
477 * Interpolate a set of points between this one and the given one
478 * @param inEndPoint end point of interpolation
479 * @param inNumPoints number of points to generate
480 * @return the DataPoint array
482 public DataPoint[] interpolate(DataPoint inEndPoint, int inNumPoints)
484 DataPoint[] range = new DataPoint[inNumPoints];
486 for (int i=0; i<inNumPoints; i++)
488 Coordinate latitude = Coordinate.interpolate(_latitude, inEndPoint.getLatitude(), i, inNumPoints);
489 Coordinate longitude = Coordinate.interpolate(_longitude, inEndPoint.getLongitude(), i, inNumPoints);
490 Altitude altitude = Altitude.interpolate(_altitude, inEndPoint.getAltitude(), i, inNumPoints);
491 range[i] = new DataPoint(latitude, longitude, altitude);
497 * Interpolate between the two given points
498 * @param inStartPoint start point
499 * @param inEndPoint end point
500 * @param inFrac fractional distance from first point (0.0 to 1.0)
501 * @return new DataPoint object between two given ones
503 public static DataPoint interpolate(DataPoint inStartPoint, DataPoint inEndPoint, double inFrac)
505 if (inStartPoint == null || inEndPoint == null) {return null;}
506 return new DataPoint(
507 Coordinate.interpolate(inStartPoint.getLatitude(), inEndPoint.getLatitude(), inFrac),
508 Coordinate.interpolate(inStartPoint.getLongitude(), inEndPoint.getLongitude(), inFrac),
509 Altitude.interpolate(inStartPoint.getAltitude(), inEndPoint.getAltitude(), inFrac)
514 * Calculate the number of radians between two points (for distance calculation)
515 * @param inPoint1 first point
516 * @param inPoint2 second point
517 * @return angular distance between points in radians
519 public static double calculateRadiansBetween(DataPoint inPoint1, DataPoint inPoint2)
521 if (inPoint1 == null || inPoint2 == null)
523 final double TO_RADIANS = Math.PI / 180.0;
524 // Get lat and long from points
525 double lat1 = inPoint1.getLatitude().getDouble() * TO_RADIANS;
526 double lat2 = inPoint2.getLatitude().getDouble() * TO_RADIANS;
527 double lon1 = inPoint1.getLongitude().getDouble() * TO_RADIANS;
528 double lon2 = inPoint2.getLongitude().getDouble() * TO_RADIANS;
529 // Formula given by Wikipedia:Great-circle_distance as follows:
530 // angle = 2 arcsin( sqrt( (sin ((lat2-lat1)/2))^^2 + cos(lat1)cos(lat2)(sin((lon2-lon1)/2))^^2))
531 double firstSine = Math.sin((lat2-lat1) / 2.0);
532 double secondSine = Math.sin((lon2-lon1) / 2.0);
533 double term2 = Math.cos(lat1) * Math.cos(lat2) * secondSine * secondSine;
534 double answer = 2 * Math.asin(Math.sqrt(firstSine*firstSine + term2));
541 * Resize the value array
542 * @param inNewIndex new index to allow
544 private void resizeValueArray(int inNewIndex)
546 int newSize = inNewIndex + 1;
547 if (newSize > _fieldValues.length)
549 String[] newArray = new String[newSize];
550 System.arraycopy(_fieldValues, 0, newArray, 0, _fieldValues.length);
551 _fieldValues = newArray;
557 * @return a clone object with copied data
559 public DataPoint clonePoint()
561 // Copy all values (note that photo not copied)
562 String[] valuesCopy = new String[_fieldValues.length];
563 System.arraycopy(_fieldValues, 0, valuesCopy, 0, _fieldValues.length);
565 PointCreateOptions options = new PointCreateOptions();
566 if (_altitude != null) {
567 options.setAltitudeUnits(_altitude.getUnit());
569 // Make new object to hold cloned data
570 DataPoint point = new DataPoint(valuesCopy, _fieldList, options);
571 // Copy the speed information
573 point.getHSpeed().copyFrom(_hSpeed);
576 point.getVSpeed().copyFrom(_vSpeed);
583 * Remove all single and double quotes surrounding each value
584 * @param inValues array of values
586 private static void removeQuotes(String[] inValues)
588 if (inValues == null) {return;}
589 for (int i=0; i<inValues.length; i++)
591 inValues[i] = removeQuotes(inValues[i]);
596 * Remove any single or double quotes surrounding a value
597 * @param inValue value to modify
598 * @return modified String
600 private static String removeQuotes(String inValue)
602 if (inValue == null) {return inValue;}
603 final int len = inValue.length();
604 if (len <= 1) {return inValue;}
605 // get the first and last characters
606 final char firstChar = inValue.charAt(0);
607 final char lastChar = inValue.charAt(len-1);
608 if (firstChar == lastChar)
610 if (firstChar == '\"' || firstChar == '\'') {
611 return inValue.substring(1, len-1);
618 * Get string for debug
619 * @see java.lang.Object#toString()
621 public String toString()
623 return "[Lat=" + getLatitude().toString() + ", Lon=" + getLongitude().toString() + "]";