]> gitweb.fperrin.net Git - GpsPrune.git/blob - src/tim/prune/data/DataPoint.java
Add menu item to remove altitudes from track
[GpsPrune.git] / src / tim / prune / data / DataPoint.java
1 package tim.prune.data;
2
3 import tim.prune.config.Config;
4
5 /**
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
9  */
10 public class DataPoint
11 {
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;
21         /** Attached photo */
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;
29
30
31         /**
32          * Constructor
33          * @param inValueArray array of String values
34          * @param inFieldList list of fields
35          * @param inOptions creation options such as units
36          */
37         public DataPoint(String[] inValueArray, FieldList inFieldList, PointCreateOptions inOptions)
38         {
39                 // save data
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);
47         }
48
49
50         /**
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
54          */
55         private void parseFields(Field inField, PointCreateOptions inOptions)
56         {
57                 if (inOptions == null) inOptions = new PointCreateOptions();
58                 if (inField == null || inField == Field.LATITUDE) {
59                         _latitude = new Latitude(getFieldValue(Field.LATITUDE));
60                 }
61                 if (inField == null || inField == Field.LONGITUDE) {
62                         _longitude = new Longitude(getFieldValue(Field.LONGITUDE));
63                 }
64                 if (inField == null || inField == Field.ALTITUDE)
65                 {
66                         Unit altUnit = inOptions.getAltitudeUnits();
67                         if (_altitude != null && _altitude.getUnit() != null) {
68                                 altUnit = _altitude.getUnit();
69                         }
70                         _altitude = new Altitude(getFieldValue(Field.ALTITUDE), altUnit);
71                 }
72                 if (inField == null || inField == Field.SPEED)
73                 {
74                         _hSpeed = new Speed(getFieldValue(Field.SPEED), inOptions.getSpeedUnits());
75                 }
76                 if (inField == null || inField == Field.VERTICAL_SPEED)
77                 {
78                         _vSpeed = new Speed(getFieldValue(Field.VERTICAL_SPEED), inOptions.getVerticalSpeedUnits());
79                         if (!inOptions.getVerticalSpeedsUpwards()) {
80                                 _vSpeed.invert();
81                         }
82                 }
83                 if (inField == null || inField == Field.TIMESTAMP) {
84                         _timestamp = new TimestampUtc(getFieldValue(Field.TIMESTAMP));
85                 }
86                 if (inField == null || inField == Field.WAYPT_NAME) {
87                         _waypointName = getFieldValue(Field.WAYPT_NAME);
88                 }
89                 if (inField == null || inField == Field.NEW_SEGMENT)
90                 {
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")));
94                 }
95         }
96
97
98         /**
99          * Constructor for additional points (eg interpolated, photos)
100          * @param inLatitude latitude
101          * @param inLongitude longitude
102          * @param inAltitude altitude
103          */
104         public DataPoint(Coordinate inLatitude, Coordinate inLongitude, Altitude inAltitude)
105         {
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;
116                 }
117                 else {
118                         _altitude = inAltitude;
119                         _fieldValues[2] = "" + inAltitude.getValue();
120                 }
121                 _timestamp = new TimestampUtc(null);
122         }
123
124
125         /**
126          * Get the value for the given field
127          * @param inField field to interrogate
128          * @return value of field
129          */
130         public String getFieldValue(Field inField)
131         {
132                 return getFieldValue(_fieldList.getFieldIndex(inField));
133         }
134
135
136         /**
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
140          */
141         private String getFieldValue(int inIndex)
142         {
143                 if (_fieldValues == null || inIndex < 0 || inIndex >= _fieldValues.length)
144                         return null;
145                 return _fieldValues[inIndex];
146         }
147
148
149         /**
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
154          */
155         public void setFieldValue(Field inField, String inValue, boolean inUndo)
156         {
157                 // See if this data point already has this field
158                 int fieldIndex = _fieldList.getFieldIndex(inField);
159                 // Add to field list if necessary
160                 if (fieldIndex < 0)
161                 {
162                         // If value is empty & field doesn't exist then do nothing
163                         if (inValue == null || inValue.equals(""))
164                         {
165                                 return;
166                         }
167                         // value isn't empty so extend field list
168                         fieldIndex = _fieldList.extendList(inField);
169                 }
170                 // Extend array of field values if necessary
171                 if (fieldIndex >= _fieldValues.length)
172                 {
173                         resizeValueArray(fieldIndex);
174                 }
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) {
179                         setModified(inUndo);
180                 }
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
185                 }
186                 else {
187                         // use default altitude format from config
188                         parseFields(inField, Config.getUnitSet().getDefaultOptions());
189                 }
190         }
191
192         /**
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
195          */
196         public void setModified(boolean inUndo)
197         {
198                 if (!inUndo) {
199                         _modifyCount++;
200                 }
201                 else {
202                         _modifyCount--;
203                 }
204         }
205
206         /**
207          * @return field list for this point
208          */
209         public FieldList getFieldList()
210         {
211                 return _fieldList;
212         }
213
214         /** @param inFlag true for start of track segment */
215         public void setSegmentStart(boolean inFlag)
216         {
217                 setFieldValue(Field.NEW_SEGMENT, inFlag?"1":null, false);
218         }
219
220         /**
221          * Mark the point for deletion
222          * @param inFlag true to delete, false to keep
223          */
224         public void setMarkedForDeletion(boolean inFlag) {
225                 _markedForDeletion = inFlag;
226         }
227
228         /** @return latitude */
229         public Coordinate getLatitude()
230         {
231                 return _latitude;
232         }
233         /** @return longitude */
234         public Coordinate getLongitude()
235         {
236                 return _longitude;
237         }
238         /** @return true if point has altitude */
239         public boolean hasAltitude()
240         {
241                 return _altitude != null && _altitude.isValid();
242         }
243         /** @return altitude */
244         public Altitude getAltitude()
245         {
246                 return _altitude;
247         }
248         /** @return true if point has horizontal speed (loaded as field) */
249         public boolean hasHSpeed()
250         {
251                 return _hSpeed != null && _hSpeed.isValid();
252         }
253         /** @return horizontal speed */
254         public Speed getHSpeed()
255         {
256                 return _hSpeed;
257         }
258         /** @return true if point has vertical speed (loaded as field) */
259         public boolean hasVSpeed()
260         {
261                 return _vSpeed != null && _vSpeed.isValid();
262         }
263         /** @return vertical speed */
264         public Speed getVSpeed()
265         {
266                 return _vSpeed;
267         }
268         /** @return true if point has timestamp */
269         public boolean hasTimestamp()
270         {
271                 return _timestamp.isValid();
272         }
273         /** @return timestamp */
274         public Timestamp getTimestamp()
275         {
276                 return _timestamp;
277         }
278         /** @return waypoint name, if any */
279         public String getWaypointName()
280         {
281                 return _waypointName;
282         }
283
284         /** @return true if start of new track segment */
285         public boolean getSegmentStart()
286         {
287                 return _startOfSegment;
288         }
289
290         /** @return true if point marked for deletion */
291         public boolean getDeleteFlag()
292         {
293                 return _markedForDeletion;
294         }
295
296         /**
297          * @return true if point has a waypoint name
298          */
299         public boolean isWaypoint()
300         {
301                 return (_waypointName != null && !_waypointName.equals(""));
302         }
303
304         /**
305          * @return true if point has been modified since loading
306          */
307         public boolean isModified()
308         {
309                 return _modifyCount > 0;
310         }
311
312         /**
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
316          */
317         public boolean isDuplicate(DataPoint inOther)
318         {
319                 if (inOther == null) return false;
320                 if (_longitude == null || _latitude == null
321                         || inOther._longitude == null || inOther._latitude == null)
322                 {
323                         return false;
324                 }
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))
329                 {
330                         return false;
331                 }
332                 // Note that conversion from decimal to dms can make non-identical points into duplicates
333                 // Compare waypoint name (if any)
334                 if (!isWaypoint())
335                 {
336                         return !inOther.isWaypoint();
337                 }
338                 return (inOther._waypointName != null && inOther._waypointName.equals(_waypointName));
339         }
340
341         /**
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
346          */
347         public void addAltitudeOffset(double inOffset, Unit inUnit, int inDecimals)
348         {
349                 if (hasAltitude())
350                 {
351                         _altitude.addOffset(inOffset, inUnit, inDecimals);
352                         _fieldValues[_fieldList.getFieldIndex(Field.ALTITUDE)] = _altitude.getStringValue(null);
353                         setModified(false);
354                 }
355         }
356
357         /**
358          * Remove altitude from point
359          */
360         public void removeAltitude()
361         {
362                 _altitude = Altitude.NONE;
363                 _fieldValues[_fieldList.getFieldIndex(Field.ALTITUDE)] = _altitude.getStringValue(null);
364                 setModified(false);
365         }
366
367         /**
368          * Reset the altitude to the previous value (by an undo)
369          * @param inClone altitude object cloned from earlier
370          */
371         public void resetAltitude(Altitude inClone)
372         {
373                 _altitude.reset(inClone);
374                 _fieldValues[_fieldList.getFieldIndex(Field.ALTITUDE)] = _altitude.getStringValue(null);
375                 setModified(true);
376         }
377
378         /**
379          * Add a time offset to this point
380          * @param inOffset offset to add (-ve to subtract)
381          */
382         public void addTimeOffsetSeconds(long inOffset)
383         {
384                 if (hasTimestamp())
385                 {
386                         _timestamp.addOffsetSeconds(inOffset);
387                         _fieldValues[_fieldList.getFieldIndex(Field.TIMESTAMP)] = _timestamp.getText(null);
388                         setModified(false);
389                 }
390         }
391
392         /**
393          * Set the photo for this data point
394          * @param inPhoto Photo object
395          */
396         public void setPhoto(Photo inPhoto) {
397                 _photo = inPhoto;
398                 _modifyCount++;
399         }
400
401         /**
402          * @return associated Photo object
403          */
404         public Photo getPhoto() {
405                 return _photo;
406         }
407
408         /**
409          * Set the audio clip for this point
410          * @param inAudio audio object
411          */
412         public void setAudio(AudioClip inAudio) {
413                 _audio = inAudio;
414                 _modifyCount++;
415         }
416
417         /**
418          * @return associated audio object
419          */
420         public AudioClip getAudio() {
421                 return _audio;
422         }
423
424         /**
425          * Attach the given media object according to type
426          * @param inMedia either a photo or an audio clip
427          */
428         public void attachMedia(MediaObject inMedia)
429         {
430                 if (inMedia != null) {
431                         if (inMedia instanceof Photo) {
432                                 setPhoto((Photo) inMedia);
433                                 inMedia.setDataPoint(this);
434                         }
435                         else if (inMedia instanceof AudioClip) {
436                                 setAudio((AudioClip) inMedia);
437                                 inMedia.setDataPoint(this);
438                         }
439                 }
440         }
441
442         /**
443          * @return true if the point is valid
444          */
445         public boolean isValid()
446         {
447                 return _latitude.isValid() && _longitude.isValid();
448         }
449
450         /**
451          * @return true if the point has either a photo or audio attached
452          */
453         public boolean hasMedia() {
454                 return _photo != null || _audio != null;
455         }
456
457         /**
458          * @return name of attached photo and/or audio
459          */
460         public String getMediaName()
461         {
462                 String mediaName = null;
463                 if (_photo != null) mediaName = _photo.getName();
464                 if (_audio != null)
465                 {
466                         if (mediaName == null) {
467                                 mediaName = _audio.getName();
468                         }
469                         else {
470                                 mediaName = mediaName + ", " + _audio.getName();
471                         }
472                 }
473                 return mediaName;
474         }
475
476         /**
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
481          */
482         public DataPoint[] interpolate(DataPoint inEndPoint, int inNumPoints)
483         {
484                 DataPoint[] range = new DataPoint[inNumPoints];
485                 // Loop over points
486                 for (int i=0; i<inNumPoints; i++)
487                 {
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);
492                 }
493                 return range;
494         }
495
496         /**
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
502          */
503         public static DataPoint interpolate(DataPoint inStartPoint, DataPoint inEndPoint, double inFrac)
504         {
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)
510                 );
511         }
512
513         /**
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
518          */
519         public static double calculateRadiansBetween(DataPoint inPoint1, DataPoint inPoint2)
520         {
521                 if (inPoint1 == null || inPoint2 == null)
522                         return 0.0;
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));
535                 // phew
536                 return answer;
537         }
538
539
540         /**
541          * Resize the value array
542          * @param inNewIndex new index to allow
543          */
544         private void resizeValueArray(int inNewIndex)
545         {
546                 int newSize = inNewIndex + 1;
547                 if (newSize > _fieldValues.length)
548                 {
549                         String[] newArray = new String[newSize];
550                         System.arraycopy(_fieldValues, 0, newArray, 0, _fieldValues.length);
551                         _fieldValues = newArray;
552                 }
553         }
554
555
556         /**
557          * @return a clone object with copied data
558          */
559         public DataPoint clonePoint()
560         {
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);
564
565                 PointCreateOptions options = new PointCreateOptions();
566                 if (_altitude != null) {
567                         options.setAltitudeUnits(_altitude.getUnit());
568                 }
569                 // Make new object to hold cloned data
570                 DataPoint point = new DataPoint(valuesCopy, _fieldList, options);
571                 // Copy the speed information
572                 if (hasHSpeed()) {
573                         point.getHSpeed().copyFrom(_hSpeed);
574                 }
575                 if (hasVSpeed()) {
576                         point.getVSpeed().copyFrom(_vSpeed);
577                 }
578                 return point;
579         }
580
581
582         /**
583          * Remove all single and double quotes surrounding each value
584          * @param inValues array of values
585          */
586         private static void removeQuotes(String[] inValues)
587         {
588                 if (inValues == null) {return;}
589                 for (int i=0; i<inValues.length; i++)
590                 {
591                         inValues[i] = removeQuotes(inValues[i]);
592                 }
593         }
594
595         /**
596          * Remove any single or double quotes surrounding a value
597          * @param inValue value to modify
598          * @return modified String
599          */
600         private static String removeQuotes(String inValue)
601         {
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)
609                 {
610                         if (firstChar == '\"' || firstChar == '\'') {
611                                 return inValue.substring(1, len-1);
612                         }
613                 }
614                 return inValue;
615         }
616
617         /**
618          * Get string for debug
619          * @see java.lang.Object#toString()
620          */
621         public String toString()
622         {
623                 return "[Lat=" + getLatitude().toString() + ", Lon=" + getLongitude().toString() + "]";
624         }
625 }