]> gitweb.fperrin.net Git - GpsPrune.git/blob - src/tim/prune/data/Track.java
f45854a0e3d47a1fd5fc0ee6924732b3294decc0
[GpsPrune.git] / src / tim / prune / data / Track.java
1 package tim.prune.data;
2
3 import java.util.List;
4
5 import tim.prune.UpdateMessageBroker;
6 import tim.prune.function.edit.FieldEdit;
7 import tim.prune.function.edit.FieldEditList;
8 import tim.prune.gui.map.MapUtils;
9
10
11 /**
12  * Class to hold all track information,
13  * including track points and waypoints
14  */
15 public class Track
16 {
17         // Data points
18         private DataPoint[] _dataPoints = null;
19         // Scaled x, y values
20         private double[] _xValues = null;
21         private double[] _yValues = null;
22         private boolean _scaled = false;
23         private int _numPoints = 0;
24         private boolean _hasTrackpoint = false;
25         private boolean _hasWaypoint = false;
26         // Master field list
27         private FieldList _masterFieldList = null;
28         // variable ranges
29         private DoubleRange _latRange = null, _longRange = null;
30         private DoubleRange _xRange = null, _yRange = null;
31
32
33         /**
34          * Constructor for empty track
35          */
36         public Track()
37         {
38                 // create field list
39                 _masterFieldList = new FieldList(null);
40                 // make empty DataPoint array
41                 _dataPoints = new DataPoint[0];
42                 _numPoints = 0;
43                 // needs to be scaled
44                 _scaled = false;
45         }
46
47         /**
48          * Constructor using fields and points from another Track
49          * @param inFieldList Field list from another Track object
50          * @param inPoints (edited) point array
51          */
52         public Track(FieldList inFieldList, DataPoint[] inPoints)
53         {
54                 _masterFieldList = inFieldList;
55                 _dataPoints = inPoints;
56                 if (_dataPoints == null) _dataPoints = new DataPoint[0];
57                 _numPoints = _dataPoints.length;
58                 _scaled = false;
59         }
60
61         /**
62          * Load method, for initialising and reinitialising data
63          * @param inFieldArray array of Field objects describing fields
64          * @param inPointArray 2d object array containing data
65          * @param inOptions load options such as units
66          */
67         public void load(Field[] inFieldArray, Object[][] inPointArray, PointCreateOptions inOptions)
68         {
69                 if (inFieldArray == null || inPointArray == null)
70                 {
71                         _numPoints = 0;
72                         return;
73                 }
74                 // copy field list
75                 _masterFieldList = new FieldList(inFieldArray);
76                 // make DataPoint object from each point in inPointList
77                 _dataPoints = new DataPoint[inPointArray.length];
78                 String[] dataArray = null;
79                 int pointIndex = 0;
80                 for (int p=0; p < inPointArray.length; p++)
81                 {
82                         dataArray = (String[]) inPointArray[p];
83                         // Convert to DataPoint objects
84                         DataPoint point = new DataPoint(dataArray, _masterFieldList, inOptions);
85                         if (point.isValid())
86                         {
87                                 _dataPoints[pointIndex] = point;
88                                 pointIndex++;
89                         }
90                         else
91                         {
92                                 // TODO: Maybe report this somehow?
93                                 // System.out.println("point is not valid!");
94                         }
95                 }
96                 _numPoints = pointIndex;
97                 // Set first track point to be start of segment
98                 DataPoint firstTrackPoint = getNextTrackPoint(0);
99                 if (firstTrackPoint != null) {
100                         firstTrackPoint.setSegmentStart(true);
101                 }
102                 // needs to be scaled
103                 _scaled = false;
104         }
105
106
107         /**
108          * Load the track by transferring the contents from a loaded Track object
109          * @param inOther Track object containing loaded data
110          */
111         public void load(Track inOther)
112         {
113                 _numPoints = inOther._numPoints;
114                 _masterFieldList = inOther._masterFieldList;
115                 _dataPoints = inOther._dataPoints;
116                 // needs to be scaled
117                 _scaled = false;
118         }
119
120         /**
121          * Request that a rescale be done to recalculate derived values
122          */
123         public void requestRescale()
124         {
125                 _scaled = false;
126         }
127
128         /**
129          * Extend the track's field list with the given additional fields
130          * @param inFieldList list of fields to be added
131          */
132         public void extendFieldList(FieldList inFieldList)
133         {
134                 _masterFieldList = _masterFieldList.merge(inFieldList);
135         }
136
137         ////////////////// Modification methods //////////////////////
138
139
140         /**
141          * Combine this Track with new data
142          * @param inOtherTrack other track to combine
143          */
144         public void combine(Track inOtherTrack)
145         {
146                 // merge field list
147                 _masterFieldList = _masterFieldList.merge(inOtherTrack._masterFieldList);
148                 // expand data array and add other track's data points
149                 int totalPoints = getNumPoints() + inOtherTrack.getNumPoints();
150                 DataPoint[] mergedPoints = new DataPoint[totalPoints];
151                 System.arraycopy(_dataPoints, 0, mergedPoints, 0, getNumPoints());
152                 System.arraycopy(inOtherTrack._dataPoints, 0, mergedPoints, getNumPoints(), inOtherTrack.getNumPoints());
153                 _dataPoints = mergedPoints;
154                 // combine point count
155                 _numPoints = totalPoints;
156                 // needs to be scaled again
157                 _scaled = false;
158                 // inform listeners
159                 UpdateMessageBroker.informSubscribers();
160         }
161
162
163         /**
164          * Crop the track to the given size - subsequent points are not (yet) deleted
165          * @param inNewSize new number of points in track
166          */
167         public void cropTo(int inNewSize)
168         {
169                 if (inNewSize >= 0 && inNewSize < getNumPoints())
170                 {
171                         _numPoints = inNewSize;
172                         // needs to be scaled again
173                         _scaled = false;
174                         UpdateMessageBroker.informSubscribers();
175                 }
176         }
177
178
179         /**
180          * Delete the points marked for deletion
181          * @param inSplitSegments true to split segments at deleted points
182          * @return number of points deleted
183          */
184         public int deleteMarkedPoints(boolean inSplitSegments)
185         {
186                 int numCopied = 0;
187                 // Copy selected points into a new point array
188                 DataPoint[] newPointArray = new DataPoint[_numPoints];
189                 boolean prevPointDeleted = false;
190                 for (int i=0; i<_numPoints; i++)
191                 {
192                         DataPoint point = _dataPoints[i];
193                         // Don't delete photo points
194                         if (point.hasMedia() || !point.getDeleteFlag())
195                         {
196                                 if (prevPointDeleted && inSplitSegments) {
197                                         point.setSegmentStart(true);
198                                 }
199                                 newPointArray[numCopied] = point;
200                                 numCopied++;
201                                 prevPointDeleted = false;
202                         }
203                         else {
204                                 prevPointDeleted = true;
205                         }
206                 }
207
208                 // Copy array references
209                 int numDeleted = _numPoints - numCopied;
210                 if (numDeleted > 0)
211                 {
212                         _dataPoints = new DataPoint[numCopied];
213                         System.arraycopy(newPointArray, 0, _dataPoints, 0, numCopied);
214                         _numPoints = _dataPoints.length;
215                         _scaled = false;
216                 }
217                 return numDeleted;
218         }
219
220
221         /**
222          * Delete the specified point
223          * @param inIndex point index
224          * @return true if successful
225          */
226         public boolean deletePoint(int inIndex)
227         {
228                 boolean answer = deleteRange(inIndex, inIndex);
229                 return answer;
230         }
231
232
233         /**
234          * Delete the specified range of points from the Track
235          * @param inStart start of range (inclusive)
236          * @param inEnd end of range (inclusive)
237          * @return true if successful
238          */
239         public boolean deleteRange(int inStart, int inEnd)
240         {
241                 if (inStart < 0 || inEnd < 0 || inEnd < inStart)
242                 {
243                         // no valid range selected so can't delete
244                         return false;
245                 }
246                 // check through range to be deleted, and see if any new segment flags present
247                 boolean hasSegmentStart = false;
248                 DataPoint nextTrackPoint = getNextTrackPoint(inEnd+1);
249                 if (nextTrackPoint != null) {
250                         for (int i=inStart; i<=inEnd && !hasSegmentStart; i++) {
251                                 hasSegmentStart |= _dataPoints[i].getSegmentStart();
252                         }
253                         // If segment break found, make sure next trackpoint also has break
254                         if (hasSegmentStart) {nextTrackPoint.setSegmentStart(true);}
255                 }
256                 // valid range, let's delete it
257                 int numToDelete = inEnd - inStart + 1;
258                 DataPoint[] newPointArray = new DataPoint[_numPoints - numToDelete];
259                 // Copy points before the selected range
260                 if (inStart > 0)
261                 {
262                         System.arraycopy(_dataPoints, 0, newPointArray, 0, inStart);
263                 }
264                 // Copy points after the deleted one(s)
265                 if (inEnd < (_numPoints - 1))
266                 {
267                         System.arraycopy(_dataPoints, inEnd + 1, newPointArray, inStart,
268                                 _numPoints - inEnd - 1);
269                 }
270                 // Copy points over original array
271                 _dataPoints = newPointArray;
272                 _numPoints -= numToDelete;
273                 // needs to be scaled again
274                 _scaled = false;
275                 return true;
276         }
277
278
279         /**
280          * Reverse the specified range of points
281          * @param inStart start index
282          * @param inEnd end index
283          * @return true if successful, false otherwise
284          */
285         public boolean reverseRange(int inStart, int inEnd)
286         {
287                 if (inStart < 0 || inEnd < 0 || inStart >= inEnd || inEnd >= _numPoints)
288                 {
289                         return false;
290                 }
291                 // calculate how many point swaps are required
292                 int numPointsToReverse = (inEnd - inStart + 1) / 2;
293                 DataPoint p = null;
294                 for (int i=0; i<numPointsToReverse; i++)
295                 {
296                         // swap pairs of points
297                         p = _dataPoints[inStart + i];
298                         _dataPoints[inStart + i] = _dataPoints[inEnd - i];
299                         _dataPoints[inEnd - i] = p;
300                 }
301                 // adjust segment starts
302                 shiftSegmentStarts(inStart, inEnd);
303                 // Find first track point and following track point, and set segment starts to true
304                 DataPoint firstTrackPoint = getNextTrackPoint(inStart);
305                 if (firstTrackPoint != null) {firstTrackPoint.setSegmentStart(true);}
306                 DataPoint nextTrackPoint = getNextTrackPoint(inEnd+1);
307                 if (nextTrackPoint != null) {nextTrackPoint.setSegmentStart(true);}
308                 // needs to be scaled again
309                 _scaled = false;
310                 UpdateMessageBroker.informSubscribers();
311                 return true;
312         }
313
314
315         /**
316          * Add the given time offset to the specified range
317          * @param inStart start of range
318          * @param inEnd end of range
319          * @param inOffset offset to add (-ve to subtract)
320          * @param inUndo true for undo operation
321          * @return true on success
322          */
323         public boolean addTimeOffsetSeconds(int inStart, int inEnd, long inOffset, boolean inUndo)
324         {
325                 // sanity check
326                 if (inStart < 0 || inEnd < 0 || inStart >= inEnd || inEnd >= _numPoints) {
327                         return false;
328                 }
329                 boolean foundTimestamp = false;
330                 // Loop over all points within range
331                 for (int i=inStart; i<=inEnd; i++)
332                 {
333                         DataPoint p = _dataPoints[i];
334                         if (p != null && p.hasTimestamp())
335                         {
336                                 // This point has a timestamp so add the offset to it
337                                 foundTimestamp = true;
338                                 p.addTimeOffsetSeconds(inOffset);
339                                 p.setModified(inUndo);
340                         }
341                 }
342                 return foundTimestamp;
343         }
344
345         /**
346          * Add the given altitude offset to the specified range
347          * @param inStart start of range
348          * @param inEnd end of range
349          * @param inOffset offset to add (-ve to subtract)
350          * @param inUnit altitude unit of offset
351          * @param inDecimals number of decimal places in offset
352          * @return true on success
353          */
354         public boolean addAltitudeOffset(int inStart, int inEnd, double inOffset,
355          Unit inUnit, int inDecimals)
356         {
357                 // sanity check
358                 if (inStart < 0 || inEnd < 0 || inStart >= inEnd || inEnd >= _numPoints) {
359                         return false;
360                 }
361                 boolean foundAlt = false;
362                 // Loop over all points within range
363                 for (int i=inStart; i<=inEnd; i++)
364                 {
365                         DataPoint p = _dataPoints[i];
366                         if (p != null && p.hasAltitude())
367                         {
368                                 // This point has an altitude so add the offset to it
369                                 foundAlt = true;
370                                 p.addAltitudeOffset(inOffset, inUnit, inDecimals);
371                                 p.setModified(false);
372                         }
373                 }
374                 // needs to be scaled again
375                 _scaled = false;
376                 return foundAlt;
377         }
378
379
380         /**
381          * Interleave all waypoints by each nearest track point
382          * @return true if successful, false if no change
383          */
384         public boolean interleaveWaypoints()
385         {
386                 // Separate waypoints and find nearest track point
387                 int numWaypoints = 0;
388                 DataPoint[] waypoints = new DataPoint[_numPoints];
389                 int[] pointIndices = new int[_numPoints];
390                 DataPoint point = null;
391                 int i = 0;
392                 for (i=0; i<_numPoints; i++)
393                 {
394                         point = _dataPoints[i];
395                         if (point.isWaypoint())
396                         {
397                                 waypoints[numWaypoints] = point;
398                                 pointIndices[numWaypoints] = getNearestPointIndex(
399                                         _xValues[i], _yValues[i], -1.0, true);
400                                 numWaypoints++;
401                         }
402                 }
403                 // Exit if data not mixed
404                 if (numWaypoints == 0 || numWaypoints == _numPoints)
405                         return false;
406
407                 // Loop round points copying to correct order
408                 DataPoint[] dataCopy = new DataPoint[_numPoints];
409                 int copyIndex = 0;
410                 for (i=0; i<_numPoints; i++)
411                 {
412                         point = _dataPoints[i];
413                         // if it's a track point, copy it
414                         if (!point.isWaypoint())
415                         {
416                                 dataCopy[copyIndex] = point;
417                                 copyIndex++;
418                         }
419                         // check for waypoints with this index
420                         for (int j=0; j<numWaypoints; j++)
421                         {
422                                 if (pointIndices[j] == i)
423                                 {
424                                         dataCopy[copyIndex] = waypoints[j];
425                                         copyIndex++;
426                                 }
427                         }
428                 }
429                 // Copy data back to track
430                 _dataPoints = dataCopy;
431                 // needs to be scaled again to recalc x, y
432                 _scaled = false;
433                 UpdateMessageBroker.informSubscribers();
434                 return true;
435         }
436
437
438         /**
439          * Cut and move the specified section
440          * @param inSectionStart start index of section
441          * @param inSectionEnd end index of section
442          * @param inMoveTo index of move to point
443          * @return true if move successful
444          */
445         public boolean cutAndMoveSection(int inSectionStart, int inSectionEnd, int inMoveTo)
446         {
447                 // TODO: Move cut/move into separate function?
448                 // Check that indices make sense
449                 if (inSectionStart >= 0 && inSectionEnd > inSectionStart && inMoveTo >= 0
450                         && (inMoveTo < inSectionStart || inMoveTo > (inSectionEnd+1)))
451                 {
452                         // do the cut and move
453                         DataPoint[] newPointArray = new DataPoint[_numPoints];
454                         // System.out.println("Cut/move section (" + inSectionStart + " - " + inSectionEnd + ") to before point " + inMoveTo);
455                         // Is it a forward copy or a backward copy?
456                         if (inSectionStart > inMoveTo)
457                         {
458                                 int sectionLength = inSectionEnd - inSectionStart + 1;
459                                 // move section to earlier point
460                                 if (inMoveTo > 0) {
461                                         System.arraycopy(_dataPoints, 0, newPointArray, 0, inMoveTo); // unchanged points before
462                                 }
463                                 System.arraycopy(_dataPoints, inSectionStart, newPointArray, inMoveTo, sectionLength); // moved bit
464                                 // after insertion point, before moved bit
465                                 System.arraycopy(_dataPoints, inMoveTo, newPointArray, inMoveTo + sectionLength, inSectionStart - inMoveTo);
466                                 // after moved bit
467                                 if (inSectionEnd < (_numPoints - 1)) {
468                                         System.arraycopy(_dataPoints, inSectionEnd+1, newPointArray, inSectionEnd+1, _numPoints - inSectionEnd - 1);
469                                 }
470                         }
471                         else
472                         {
473                                 // Move section to later point
474                                 if (inSectionStart > 0) {
475                                         System.arraycopy(_dataPoints, 0, newPointArray, 0, inSectionStart); // unchanged points before
476                                 }
477                                 // from end of section to move to point
478                                 if (inMoveTo > (inSectionEnd + 1)) {
479                                         System.arraycopy(_dataPoints, inSectionEnd+1, newPointArray, inSectionStart, inMoveTo - inSectionEnd - 1);
480                                 }
481                                 // moved bit
482                                 System.arraycopy(_dataPoints, inSectionStart, newPointArray, inSectionStart + inMoveTo - inSectionEnd - 1,
483                                         inSectionEnd - inSectionStart + 1);
484                                 // unchanged bit after
485                                 if (inSectionEnd < (_numPoints - 1)) {
486                                         System.arraycopy(_dataPoints, inMoveTo, newPointArray, inMoveTo, _numPoints - inMoveTo);
487                                 }
488                         }
489                         // Copy array references
490                         _dataPoints = newPointArray;
491                         _scaled = false;
492                         return true;
493                 }
494                 return false;
495         }
496
497
498         /**
499          * Interpolate extra points between two selected ones
500          * @param inStartIndex start index of interpolation
501          * @param inNumPoints num points to insert
502          * @return true if successful
503          */
504         public boolean interpolate(int inStartIndex, int inNumPoints)
505         {
506                 // check parameters
507                 if (inStartIndex < 0 || inStartIndex >= _numPoints || inNumPoints <= 0)
508                         return false;
509
510                 // get start and end points
511                 DataPoint startPoint = getPoint(inStartIndex);
512                 DataPoint endPoint = getPoint(inStartIndex + 1);
513
514                 // Make array of points to insert
515                 DataPoint[] insertedPoints = startPoint.interpolate(endPoint, inNumPoints);
516
517                 // Insert points into track
518                 return insertRange(insertedPoints, inStartIndex + 1);
519         }
520
521
522         /**
523          * Average selected points
524          * @param inStartIndex start index of selection
525          * @param inEndIndex end index of selection
526          * @return true if successful
527          */
528         public boolean average(int inStartIndex, int inEndIndex)
529         {
530                 // check parameters
531                 if (inStartIndex < 0 || inStartIndex >= _numPoints || inEndIndex <= inStartIndex)
532                         return false;
533
534                 DataPoint startPoint = getPoint(inStartIndex);
535                 double firstLatitude = startPoint.getLatitude().getDouble();
536                 double firstLongitude = startPoint.getLongitude().getDouble();
537                 double latitudeDiff = 0.0, longitudeDiff = 0.0;
538                 double totalAltitude = 0;
539                 int numAltitudes = 0;
540                 Unit altUnit = null;
541                 // loop between start and end points
542                 for (int i=inStartIndex; i<= inEndIndex; i++)
543                 {
544                         DataPoint currPoint = getPoint(i);
545                         latitudeDiff += (currPoint.getLatitude().getDouble() - firstLatitude);
546                         longitudeDiff += (currPoint.getLongitude().getDouble() - firstLongitude);
547                         if (currPoint.hasAltitude())
548                         {
549                                 totalAltitude += currPoint.getAltitude().getValue(altUnit);
550                                 // Use altitude format of first valid altitude
551                                 if (altUnit == null)
552                                         altUnit = currPoint.getAltitude().getUnit();
553                                 numAltitudes++;
554                         }
555                 }
556                 int numPoints = inEndIndex - inStartIndex + 1;
557                 double meanLatitude = firstLatitude + (latitudeDiff / numPoints);
558                 double meanLongitude = firstLongitude + (longitudeDiff / numPoints);
559                 Altitude meanAltitude = null;
560                 if (numAltitudes > 0) {
561                         meanAltitude = new Altitude((int) (totalAltitude / numAltitudes), altUnit);
562                 }
563
564                 DataPoint insertedPoint = new DataPoint(new Latitude(meanLatitude, Coordinate.FORMAT_DECIMAL_FORCE_POINT),
565                         new Longitude(meanLongitude, Coordinate.FORMAT_DECIMAL_FORCE_POINT), meanAltitude);
566                 // Make into singleton
567                 insertedPoint.setSegmentStart(true);
568                 DataPoint nextPoint = getNextTrackPoint(inEndIndex+1);
569                 if (nextPoint != null) {nextPoint.setSegmentStart(true);}
570                 // Insert points into track
571                 return insertRange(new DataPoint[] {insertedPoint}, inEndIndex + 1);
572         }
573
574
575         /**
576          * Append the specified points to the end of the track
577          * @param inPoints DataPoint objects to add
578          */
579         public void appendPoints(DataPoint[] inPoints)
580         {
581                 // Insert points into track
582                 if (inPoints != null && inPoints.length > 0)
583                 {
584                         insertRange(inPoints, _numPoints);
585                 }
586                 // needs to be scaled again to recalc x, y
587                 _scaled = false;
588                 UpdateMessageBroker.informSubscribers();
589         }
590
591
592         //////// information methods /////////////
593
594
595         /**
596          * Get the point at the given index
597          * @param inPointNum index number, starting at 0
598          * @return DataPoint object, or null if out of range
599          */
600         public DataPoint getPoint(int inPointNum)
601         {
602                 if (inPointNum > -1 && inPointNum < getNumPoints())
603                 {
604                         return _dataPoints[inPointNum];
605                 }
606                 return null;
607         }
608
609         /**
610          * @return the number of (valid) points in the track
611          */
612         public int getNumPoints()
613         {
614                 return _numPoints;
615         }
616
617         /**
618          * @return The range of x values as a DoubleRange object
619          */
620         public DoubleRange getXRange()
621         {
622                 if (!_scaled) {scalePoints();}
623                 return _xRange;
624         }
625
626         /**
627          * @return The range of y values as a DoubleRange object
628          */
629         public DoubleRange getYRange()
630         {
631                 if (!_scaled) {scalePoints();}
632                 return _yRange;
633         }
634
635         /**
636          * @return The range of lat values as a DoubleRange object
637          */
638         public DoubleRange getLatRange()
639         {
640                 if (!_scaled) {scalePoints();}
641                 return _latRange;
642         }
643         /**
644          * @return The range of lon values as a DoubleRange object
645          */
646         public DoubleRange getLonRange()
647         {
648                 if (!_scaled) {scalePoints();}
649                 return _longRange;
650         }
651
652         /**
653          * @param inPointNum point index, starting at 0
654          * @return scaled x value of specified point
655          */
656         public double getX(int inPointNum)
657         {
658                 if (!_scaled) {scalePoints();}
659                 return _xValues[inPointNum];
660         }
661
662         /**
663          * @param inPointNum point index, starting at 0
664          * @return scaled y value of specified point
665          */
666         public double getY(int inPointNum)
667         {
668                 if (!_scaled) {scalePoints();}
669                 return _yValues[inPointNum];
670         }
671
672         /**
673          * @return the master field list
674          */
675         public FieldList getFieldList()
676         {
677                 return _masterFieldList;
678         }
679
680
681         /**
682          * Checks if any data exists for the specified field
683          * @param inField Field to examine
684          * @return true if data exists for this field
685          */
686         public boolean hasData(Field inField)
687         {
688                 // Don't use this method for altitudes
689                 if (inField.equals(Field.ALTITUDE)) {return hasAltitudeData();}
690                 return hasData(inField, 0, _numPoints-1);
691         }
692
693
694         /**
695          * Checks if any data exists for the specified field in the specified range
696          * @param inField Field to examine
697          * @param inStart start of range to check
698          * @param inEnd end of range to check (inclusive)
699          * @return true if data exists for this field
700          */
701         public boolean hasData(Field inField, int inStart, int inEnd)
702         {
703                 // Loop over selected point range
704                 for (int i=inStart; i<=inEnd; i++)
705                 {
706                         if (_dataPoints[i].getFieldValue(inField) != null)
707                         {
708                                 // Check altitudes and timestamps
709                                 if ((inField != Field.ALTITUDE || _dataPoints[i].getAltitude().isValid())
710                                         && (inField != Field.TIMESTAMP || _dataPoints[i].getTimestamp().isValid()))
711                                 {
712                                         return true;
713                                 }
714                         }
715                 }
716                 return false;
717         }
718
719         /**
720          * @return true if track has altitude data
721          */
722         public boolean hasAltitudeData()
723         {
724                 for (int i=0; i<_numPoints; i++) {
725                         if (_dataPoints[i].hasAltitude()) {return true;}
726                 }
727                 return false;
728         }
729
730         /**
731          * @return true if track contains at least one trackpoint
732          */
733         public boolean hasTrackPoints()
734         {
735                 if (!_scaled) {scalePoints();}
736                 return _hasTrackpoint;
737         }
738
739         /**
740          * @return true if track contains waypoints
741          */
742         public boolean hasWaypoints()
743         {
744                 if (!_scaled) {scalePoints();}
745                 return _hasWaypoint;
746         }
747
748         /**
749          * @return true if track contains any points marked for deletion
750          */
751         public boolean hasMarkedPoints()
752         {
753                 if (_numPoints < 1) {
754                         return false;
755                 }
756                 // Loop over points looking for any marked for deletion
757                 for (int i=0; i<=_numPoints-1; i++)
758                 {
759                         if (_dataPoints[i] != null && _dataPoints[i].getDeleteFlag()) {
760                                 return true;
761                         }
762                 }
763                 // None found
764                 return false;
765         }
766
767         /**
768          * Clear all the deletion markers
769          */
770         public void clearDeletionMarkers()
771         {
772                 for (int i=0; i<_numPoints; i++)
773                 {
774                         _dataPoints[i].setMarkedForDeletion(false);
775                 }
776         }
777
778         /**
779          * Collect all the waypoints into the given List
780          * @param inList List to fill with waypoints
781          */
782         public void getWaypoints(List<DataPoint> inList)
783         {
784                 // clear list
785                 inList.clear();
786                 // loop over points and copy all waypoints into list
787                 for (int i=0; i<=_numPoints-1; i++)
788                 {
789                         if (_dataPoints[i] != null && _dataPoints[i].isWaypoint())
790                         {
791                                 inList.add(_dataPoints[i]);
792                         }
793                 }
794         }
795
796
797         /**
798          * Search for the given Point in the track and return the index
799          * @param inPoint Point to look for
800          * @return index of Point, if any or -1 if not found
801          */
802         public int getPointIndex(DataPoint inPoint)
803         {
804                 if (inPoint != null)
805                 {
806                         // Loop over points in track
807                         for (int i=0; i<=_numPoints-1; i++)
808                         {
809                                 if (_dataPoints[i] == inPoint)
810                                 {
811                                         return i;
812                                 }
813                         }
814                 }
815                 // not found
816                 return -1;
817         }
818
819
820         ///////// Internal processing methods ////////////////
821
822
823         /**
824          * Scale all the points in the track to gain x and y values
825          * ready for plotting
826          */
827         private synchronized void scalePoints()
828         {
829                 // Loop through all points in track, to see limits of lat, long
830                 _longRange = new DoubleRange();
831                 _latRange = new DoubleRange();
832                 int p;
833                 _hasWaypoint = false; _hasTrackpoint = false;
834                 for (p=0; p < getNumPoints(); p++)
835                 {
836                         DataPoint point = getPoint(p);
837                         if (point != null && point.isValid())
838                         {
839                                 _longRange.addValue(point.getLongitude().getDouble());
840                                 _latRange.addValue(point.getLatitude().getDouble());
841                                 if (point.isWaypoint())
842                                         _hasWaypoint = true;
843                                 else
844                                         _hasTrackpoint = true;
845                         }
846                 }
847
848                 // Loop over points and calculate scales
849                 _xValues = new double[getNumPoints()];
850                 _yValues = new double[getNumPoints()];
851                 _xRange = new DoubleRange();
852                 _yRange = new DoubleRange();
853                 for (p=0; p < getNumPoints(); p++)
854                 {
855                         DataPoint point = getPoint(p);
856                         if (point != null)
857                         {
858                                 _xValues[p] = MapUtils.getXFromLongitude(point.getLongitude().getDouble());
859                                 _xRange.addValue(_xValues[p]);
860                                 _yValues[p] = MapUtils.getYFromLatitude(point.getLatitude().getDouble());
861                                 _yRange.addValue(_yValues[p]);
862                         }
863                 }
864                 _scaled = true;
865         }
866
867
868         /**
869          * Find the nearest point to the specified x and y coordinates
870          * or -1 if no point is within the specified max distance
871          * @param inX x coordinate
872          * @param inY y coordinate
873          * @param inMaxDist maximum distance from selected coordinates
874          * @param inJustTrackPoints true if waypoints should be ignored
875          * @return index of nearest point or -1 if not found
876          */
877         public int getNearestPointIndex(double inX, double inY, double inMaxDist, boolean inJustTrackPoints)
878         {
879                 int nearestPoint = 0;
880                 double nearestDist = -1.0;
881                 double mDist, yDist;
882                 for (int i=0; i < getNumPoints(); i++)
883                 {
884                         if (!inJustTrackPoints || !_dataPoints[i].isWaypoint())
885                         {
886                                 yDist = Math.abs(_yValues[i] - inY);
887                                 if (yDist < nearestDist || nearestDist < 0.0)
888                                 {
889                                         // y dist is within range, so check x too
890                                         mDist = yDist + getMinXDist(_xValues[i] - inX);
891                                         if (mDist < nearestDist || nearestDist < 0.0)
892                                         {
893                                                 nearestPoint = i;
894                                                 nearestDist = mDist;
895                                         }
896                                 }
897                         }
898                 }
899                 // Check whether it's within required distance
900                 if (nearestDist > inMaxDist && inMaxDist > 0.0)
901                 {
902                         return -1;
903                 }
904                 return nearestPoint;
905         }
906
907         /**
908          * @param inX x value of point
909          * @return minimum wrapped value
910          */
911         private static final double getMinXDist(double inX)
912         {
913                 // TODO: Should be abs(mod(inX-0.5,1)-0.5) - means two adds, one mod, one abs instead of two adds, 3 abss and two compares
914                 return Math.min(Math.min(Math.abs(inX), Math.abs(inX-1.0)), Math.abs(inX+1.0));
915         }
916
917         /**
918          * Get the next track point starting from the given index
919          * @param inStartIndex index to start looking from
920          * @return next track point, or null if end of data reached
921          */
922         public DataPoint getNextTrackPoint(int inStartIndex)
923         {
924                 return getNextTrackPoint(inStartIndex, _numPoints, true);
925         }
926
927         /**
928          * Get the next track point in the given range
929          * @param inStartIndex index to start looking from
930          * @param inEndIndex index to stop looking
931          * @return next track point, or null if end of data reached
932          */
933         public DataPoint getNextTrackPoint(int inStartIndex, int inEndIndex)
934         {
935                 return getNextTrackPoint(inStartIndex, inEndIndex, true);
936         }
937
938         /**
939          * Get the previous track point starting from the given index
940          * @param inStartIndex index to start looking from
941          * @return next track point, or null if end of data reached
942          */
943         public DataPoint getPreviousTrackPoint(int inStartIndex)
944         {
945                 // end index is given as _numPoints but actually it just counts down to -1
946                 return getNextTrackPoint(inStartIndex, _numPoints, false);
947         }
948
949         /**
950          * Get the next track point starting from the given index
951          * @param inStartIndex index to start looking from
952          * @param inEndIndex index to stop looking (inclusive)
953          * @param inCountUp true for next, false for previous
954          * @return next track point, or null if end of data reached
955          */
956         private DataPoint getNextTrackPoint(int inStartIndex, int inEndIndex, boolean inCountUp)
957         {
958                 // Loop forever over points
959                 int increment = inCountUp?1:-1;
960                 for (int i=inStartIndex; i<=inEndIndex; i+=increment)
961                 {
962                         DataPoint point = getPoint(i);
963                         // Exit if end of data reached - there wasn't a track point
964                         if (point == null) {return null;}
965                         if (point.isValid() && !point.isWaypoint()) {
966                                 // next track point found
967                                 return point;
968                         }
969                 }
970                 return null;
971         }
972
973         /**
974          * Shift all the segment start flags in the given range by 1
975          * Method used by reverse range and its undo
976          * @param inStartIndex start of range, inclusive
977          * @param inEndIndex end of range, inclusive
978          */
979         public void shiftSegmentStarts(int inStartIndex, int inEndIndex)
980         {
981                 boolean prevFlag = true;
982                 boolean currFlag = true;
983                 for (int i=inStartIndex; i<= inEndIndex; i++)
984                 {
985                         DataPoint point = getPoint(i);
986                         if (point != null && !point.isWaypoint())
987                         {
988                                 // remember flag
989                                 currFlag = point.getSegmentStart();
990                                 // shift flag by 1
991                                 point.setSegmentStart(prevFlag);
992                                 prevFlag = currFlag;
993                         }
994                 }
995         }
996
997         ////////////////// Cloning and replacing ///////////////////
998
999         /**
1000          * Clone the array of DataPoints
1001          * @return shallow copy of DataPoint objects
1002          */
1003         public DataPoint[] cloneContents()
1004         {
1005                 DataPoint[] clone = new DataPoint[getNumPoints()];
1006                 System.arraycopy(_dataPoints, 0, clone, 0, getNumPoints());
1007                 return clone;
1008         }
1009
1010
1011         /**
1012          * Clone the specified range of data points
1013          * @param inStart start index (inclusive)
1014          * @param inEnd end index (inclusive)
1015          * @return shallow copy of DataPoint objects
1016          */
1017         public DataPoint[] cloneRange(int inStart, int inEnd)
1018         {
1019                 int numSelected = 0;
1020                 if (inEnd >= 0 && inEnd >= inStart)
1021                 {
1022                         numSelected = inEnd - inStart + 1;
1023                 }
1024                 DataPoint[] result = new DataPoint[numSelected>0?numSelected:0];
1025                 if (numSelected > 0)
1026                 {
1027                         System.arraycopy(_dataPoints, inStart, result, 0, numSelected);
1028                 }
1029                 return result;
1030         }
1031
1032
1033         /**
1034          * Re-insert the specified point at the given index
1035          * @param inPoint point to insert
1036          * @param inIndex index at which to insert the point
1037          * @return true if it worked, false otherwise
1038          */
1039         public boolean insertPoint(DataPoint inPoint, int inIndex)
1040         {
1041                 if (inIndex > _numPoints || inPoint == null)
1042                 {
1043                         return false;
1044                 }
1045                 // Make new array to copy points over to
1046                 DataPoint[] newPointArray = new DataPoint[_numPoints + 1];
1047                 if (inIndex > 0)
1048                 {
1049                         System.arraycopy(_dataPoints, 0, newPointArray, 0, inIndex);
1050                 }
1051                 newPointArray[inIndex] = inPoint;
1052                 if (inIndex < _numPoints)
1053                 {
1054                         System.arraycopy(_dataPoints, inIndex, newPointArray, inIndex+1, _numPoints - inIndex);
1055                 }
1056                 // Change over to new array
1057                 _dataPoints = newPointArray;
1058                 _numPoints++;
1059                 // needs to be scaled again
1060                 _scaled = false;
1061                 UpdateMessageBroker.informSubscribers();
1062                 return true;
1063         }
1064
1065
1066         /**
1067          * Re-insert the specified point range at the given index
1068          * @param inPoints point array to insert
1069          * @param inIndex index at which to insert the points
1070          * @return true if it worked, false otherwise
1071          */
1072         public boolean insertRange(DataPoint[] inPoints, int inIndex)
1073         {
1074                 if (inIndex > _numPoints || inPoints == null)
1075                 {
1076                         return false;
1077                 }
1078                 // Make new array to copy points over to
1079                 DataPoint[] newPointArray = new DataPoint[_numPoints + inPoints.length];
1080                 if (inIndex > 0)
1081                 {
1082                         System.arraycopy(_dataPoints, 0, newPointArray, 0, inIndex);
1083                 }
1084                 System.arraycopy(inPoints, 0, newPointArray, inIndex, inPoints.length);
1085                 if (inIndex < _numPoints)
1086                 {
1087                         System.arraycopy(_dataPoints, inIndex, newPointArray, inIndex+inPoints.length, _numPoints - inIndex);
1088                 }
1089                 // Change over to new array
1090                 _dataPoints = newPointArray;
1091                 _numPoints += inPoints.length;
1092                 // needs to be scaled again
1093                 _scaled = false;
1094                 UpdateMessageBroker.informSubscribers();
1095                 return true;
1096         }
1097
1098
1099         /**
1100          * Replace the track contents with the given point array
1101          * @param inContents array of DataPoint objects
1102          * @return true on success
1103          */
1104         public boolean replaceContents(DataPoint[] inContents)
1105         {
1106                 // master field array stays the same
1107                 // (would need to store field array too if we wanted to redo a load)
1108                 // replace data array
1109                 _dataPoints = inContents;
1110                 _numPoints = _dataPoints.length;
1111                 _scaled = false;
1112                 UpdateMessageBroker.informSubscribers();
1113                 return true;
1114         }
1115
1116
1117         /**
1118          * Edit the specified point
1119          * @param inPoint point to edit
1120          * @param inEditList list of edits to make
1121          * @param inUndo true if undo operation, false otherwise
1122          * @return true if successful
1123          */
1124         public boolean editPoint(DataPoint inPoint, FieldEditList inEditList, boolean inUndo)
1125         {
1126                 if (inPoint != null && inEditList != null && inEditList.getNumEdits() > 0)
1127                 {
1128                         // remember if coordinates have changed
1129                         boolean coordsChanged = false;
1130                         // go through edits one by one
1131                         int numEdits = inEditList.getNumEdits();
1132                         for (int i=0; i<numEdits; i++)
1133                         {
1134                                 FieldEdit edit = inEditList.getEdit(i);
1135                                 Field editField = edit.getField();
1136                                 inPoint.setFieldValue(editField, edit.getValue(), inUndo);
1137                                 // Check that master field list has this field already (maybe point name has been added)
1138                                 if (!_masterFieldList.contains(editField)) {
1139                                         _masterFieldList.extendList(editField);
1140                                 }
1141                                 // check coordinates
1142                                 coordsChanged |= (editField.equals(Field.LATITUDE)
1143                                         || editField.equals(Field.LONGITUDE) || editField.equals(Field.ALTITUDE));
1144                         }
1145                         // set photo status if coordinates have changed
1146                         if (inPoint.getPhoto() != null && coordsChanged)
1147                         {
1148                                 inPoint.getPhoto().setCurrentStatus(Photo.Status.CONNECTED);
1149                         }
1150                         // point possibly needs to be scaled again
1151                         _scaled = false;
1152                         // trigger listeners
1153                         UpdateMessageBroker.informSubscribers();
1154                         return true;
1155                 }
1156                 return false;
1157         }
1158
1159         /**
1160          * @param inPoint point to check
1161          * @return true if this track contains the given point
1162          */
1163         public boolean containsPoint(DataPoint inPoint)
1164         {
1165                 if (inPoint == null) return false;
1166                 for (int i=0; i < getNumPoints(); i++)
1167                 {
1168                         if (getPoint(i) == inPoint) return true;
1169                 }
1170                 return false; // not found
1171         }
1172 }