]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/data/DataPoint.java
Version 19, May 2018
[GpsPrune.git] / 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          * Reset the altitude to the previous value (by an undo)
359          * @param inClone altitude object cloned from earlier
360          */
361         public void resetAltitude(Altitude inClone)
362         {
363                 _altitude.reset(inClone);
364                 _fieldValues[_fieldList.getFieldIndex(Field.ALTITUDE)] = _altitude.getStringValue(null);
365                 setModified(true);
366         }
367
368         /**
369          * Add a time offset to this point
370          * @param inOffset offset to add (-ve to subtract)
371          */
372         public void addTimeOffsetSeconds(long inOffset)
373         {
374                 if (hasTimestamp())
375                 {
376                         _timestamp.addOffsetSeconds(inOffset);
377                         _fieldValues[_fieldList.getFieldIndex(Field.TIMESTAMP)] = _timestamp.getText(null);
378                         setModified(false);
379                 }
380         }
381
382         /**
383          * Set the photo for this data point
384          * @param inPhoto Photo object
385          */
386         public void setPhoto(Photo inPhoto) {
387                 _photo = inPhoto;
388                 _modifyCount++;
389         }
390
391         /**
392          * @return associated Photo object
393          */
394         public Photo getPhoto() {
395                 return _photo;
396         }
397
398         /**
399          * Set the audio clip for this point
400          * @param inAudio audio object
401          */
402         public void setAudio(AudioClip inAudio) {
403                 _audio = inAudio;
404                 _modifyCount++;
405         }
406
407         /**
408          * @return associated audio object
409          */
410         public AudioClip getAudio() {
411                 return _audio;
412         }
413
414         /**
415          * Attach the given media object according to type
416          * @param inMedia either a photo or an audio clip
417          */
418         public void attachMedia(MediaObject inMedia)
419         {
420                 if (inMedia != null) {
421                         if (inMedia instanceof Photo) {
422                                 setPhoto((Photo) inMedia);
423                                 inMedia.setDataPoint(this);
424                         }
425                         else if (inMedia instanceof AudioClip) {
426                                 setAudio((AudioClip) inMedia);
427                                 inMedia.setDataPoint(this);
428                         }
429                 }
430         }
431
432         /**
433          * @return true if the point is valid
434          */
435         public boolean isValid()
436         {
437                 return _latitude.isValid() && _longitude.isValid();
438         }
439
440         /**
441          * @return true if the point has either a photo or audio attached
442          */
443         public boolean hasMedia() {
444                 return _photo != null || _audio != null;
445         }
446
447         /**
448          * @return name of attached photo and/or audio
449          */
450         public String getMediaName()
451         {
452                 String mediaName = null;
453                 if (_photo != null) mediaName = _photo.getName();
454                 if (_audio != null)
455                 {
456                         if (mediaName == null) {
457                                 mediaName = _audio.getName();
458                         }
459                         else {
460                                 mediaName = mediaName + ", " + _audio.getName();
461                         }
462                 }
463                 return mediaName;
464         }
465
466         /**
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
471          */
472         public DataPoint[] interpolate(DataPoint inEndPoint, int inNumPoints)
473         {
474                 DataPoint[] range = new DataPoint[inNumPoints];
475                 // Loop over points
476                 for (int i=0; i<inNumPoints; i++)
477                 {
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);
482                 }
483                 return range;
484         }
485
486         /**
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
492          */
493         public static DataPoint interpolate(DataPoint inStartPoint, DataPoint inEndPoint, double inFrac)
494         {
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)
500                 );
501         }
502
503         /**
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
508          */
509         public static double calculateRadiansBetween(DataPoint inPoint1, DataPoint inPoint2)
510         {
511                 if (inPoint1 == null || inPoint2 == null)
512                         return 0.0;
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));
525                 // phew
526                 return answer;
527         }
528
529
530         /**
531          * Resize the value array
532          * @param inNewIndex new index to allow
533          */
534         private void resizeValueArray(int inNewIndex)
535         {
536                 int newSize = inNewIndex + 1;
537                 if (newSize > _fieldValues.length)
538                 {
539                         String[] newArray = new String[newSize];
540                         System.arraycopy(_fieldValues, 0, newArray, 0, _fieldValues.length);
541                         _fieldValues = newArray;
542                 }
543         }
544
545
546         /**
547          * @return a clone object with copied data
548          */
549         public DataPoint clonePoint()
550         {
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);
554
555                 PointCreateOptions options = new PointCreateOptions();
556                 if (_altitude != null) {
557                         options.setAltitudeUnits(_altitude.getUnit());
558                 }
559                 // Make new object to hold cloned data
560                 DataPoint point = new DataPoint(valuesCopy, _fieldList, options);
561                 // Copy the speed information
562                 if (hasHSpeed()) {
563                         point.getHSpeed().copyFrom(_hSpeed);
564                 }
565                 if (hasVSpeed()) {
566                         point.getVSpeed().copyFrom(_vSpeed);
567                 }
568                 return point;
569         }
570
571
572         /**
573          * Remove all single and double quotes surrounding each value
574          * @param inValues array of values
575          */
576         private static void removeQuotes(String[] inValues)
577         {
578                 if (inValues == null) {return;}
579                 for (int i=0; i<inValues.length; i++)
580                 {
581                         inValues[i] = removeQuotes(inValues[i]);
582                 }
583         }
584
585         /**
586          * Remove any single or double quotes surrounding a value
587          * @param inValue value to modify
588          * @return modified String
589          */
590         private static String removeQuotes(String inValue)
591         {
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)
599                 {
600                         if (firstChar == '\"' || firstChar == '\'') {
601                                 return inValue.substring(1, len-1);
602                         }
603                 }
604                 return inValue;
605         }
606
607         /**
608          * Get string for debug
609          * @see java.lang.Object#toString()
610          */
611         public String toString()
612         {
613                 return "[Lat=" + getLatitude().toString() + ", Lon=" + getLongitude().toString() + "]";
614         }
615 }