]> gitweb.fperrin.net Git - GpsPrune.git/blob - src/tim/prune/data/Track.java
Add a panel to select by segment
[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          * Remove altitudes from the specified range
382          * @param inStart start of range
383          * @param inEnd end of range
384          */
385         public boolean removeAltitudes(int inStart, int inEnd)
386         {
387                 // sanity check
388                 if (inStart < 0 || inEnd < 0 || inStart >= inEnd || inEnd >= _numPoints) {
389                         return false;
390                 }
391
392                 boolean anyRemoved = false;
393                 for (int i=inStart; i<=inEnd; i++)
394                 {
395                         DataPoint p = _dataPoints[i];
396                         if (p != null && p.hasAltitude())
397                         {
398                                 p.removeAltitude();
399                                 anyRemoved = true;
400                         }
401                 }
402                 return anyRemoved;
403         }
404
405         /**
406          * Interleave all waypoints by each nearest track point
407          * @return true if successful, false if no change
408          */
409         public boolean interleaveWaypoints()
410         {
411                 // Separate waypoints and find nearest track point
412                 int numWaypoints = 0;
413                 DataPoint[] waypoints = new DataPoint[_numPoints];
414                 int[] pointIndices = new int[_numPoints];
415                 DataPoint point = null;
416                 int i = 0;
417                 for (i=0; i<_numPoints; i++)
418                 {
419                         point = _dataPoints[i];
420                         if (point.isWaypoint())
421                         {
422                                 waypoints[numWaypoints] = point;
423                                 pointIndices[numWaypoints] = getNearestPointIndex(
424                                         _xValues[i], _yValues[i], -1.0, true);
425                                 numWaypoints++;
426                         }
427                 }
428                 // Exit if data not mixed
429                 if (numWaypoints == 0 || numWaypoints == _numPoints)
430                         return false;
431
432                 // Loop round points copying to correct order
433                 DataPoint[] dataCopy = new DataPoint[_numPoints];
434                 int copyIndex = 0;
435                 for (i=0; i<_numPoints; i++)
436                 {
437                         point = _dataPoints[i];
438                         // if it's a track point, copy it
439                         if (!point.isWaypoint())
440                         {
441                                 dataCopy[copyIndex] = point;
442                                 copyIndex++;
443                         }
444                         // check for waypoints with this index
445                         for (int j=0; j<numWaypoints; j++)
446                         {
447                                 if (pointIndices[j] == i)
448                                 {
449                                         dataCopy[copyIndex] = waypoints[j];
450                                         copyIndex++;
451                                 }
452                         }
453                 }
454                 // Copy data back to track
455                 _dataPoints = dataCopy;
456                 // needs to be scaled again to recalc x, y
457                 _scaled = false;
458                 UpdateMessageBroker.informSubscribers();
459                 return true;
460         }
461
462
463         /**
464          * Cut and move the specified section
465          * @param inSectionStart start index of section
466          * @param inSectionEnd end index of section
467          * @param inMoveTo index of move to point
468          * @return true if move successful
469          */
470         public boolean cutAndMoveSection(int inSectionStart, int inSectionEnd, int inMoveTo)
471         {
472                 // TODO: Move cut/move into separate function?
473                 // Check that indices make sense
474                 if (inSectionStart >= 0 && inSectionEnd > inSectionStart && inMoveTo >= 0
475                         && (inMoveTo < inSectionStart || inMoveTo > (inSectionEnd+1)))
476                 {
477                         // do the cut and move
478                         DataPoint[] newPointArray = new DataPoint[_numPoints];
479                         // System.out.println("Cut/move section (" + inSectionStart + " - " + inSectionEnd + ") to before point " + inMoveTo);
480                         // Is it a forward copy or a backward copy?
481                         if (inSectionStart > inMoveTo)
482                         {
483                                 int sectionLength = inSectionEnd - inSectionStart + 1;
484                                 // move section to earlier point
485                                 if (inMoveTo > 0) {
486                                         System.arraycopy(_dataPoints, 0, newPointArray, 0, inMoveTo); // unchanged points before
487                                 }
488                                 System.arraycopy(_dataPoints, inSectionStart, newPointArray, inMoveTo, sectionLength); // moved bit
489                                 // after insertion point, before moved bit
490                                 System.arraycopy(_dataPoints, inMoveTo, newPointArray, inMoveTo + sectionLength, inSectionStart - inMoveTo);
491                                 // after moved bit
492                                 if (inSectionEnd < (_numPoints - 1)) {
493                                         System.arraycopy(_dataPoints, inSectionEnd+1, newPointArray, inSectionEnd+1, _numPoints - inSectionEnd - 1);
494                                 }
495                         }
496                         else
497                         {
498                                 // Move section to later point
499                                 if (inSectionStart > 0) {
500                                         System.arraycopy(_dataPoints, 0, newPointArray, 0, inSectionStart); // unchanged points before
501                                 }
502                                 // from end of section to move to point
503                                 if (inMoveTo > (inSectionEnd + 1)) {
504                                         System.arraycopy(_dataPoints, inSectionEnd+1, newPointArray, inSectionStart, inMoveTo - inSectionEnd - 1);
505                                 }
506                                 // moved bit
507                                 System.arraycopy(_dataPoints, inSectionStart, newPointArray, inSectionStart + inMoveTo - inSectionEnd - 1,
508                                         inSectionEnd - inSectionStart + 1);
509                                 // unchanged bit after
510                                 if (inSectionEnd < (_numPoints - 1)) {
511                                         System.arraycopy(_dataPoints, inMoveTo, newPointArray, inMoveTo, _numPoints - inMoveTo);
512                                 }
513                         }
514                         // Copy array references
515                         _dataPoints = newPointArray;
516                         _scaled = false;
517                         return true;
518                 }
519                 return false;
520         }
521
522
523         /**
524          * Interpolate extra points between two selected ones
525          * @param inStartIndex start index of interpolation
526          * @param inNumPoints num points to insert
527          * @return true if successful
528          */
529         public boolean interpolate(int inStartIndex, int inNumPoints)
530         {
531                 // check parameters
532                 if (inStartIndex < 0 || inStartIndex >= _numPoints || inNumPoints <= 0)
533                         return false;
534
535                 // get start and end points
536                 DataPoint startPoint = getPoint(inStartIndex);
537                 DataPoint endPoint = getPoint(inStartIndex + 1);
538
539                 // Make array of points to insert
540                 DataPoint[] insertedPoints = startPoint.interpolate(endPoint, inNumPoints);
541
542                 // Insert points into track
543                 return insertRange(insertedPoints, inStartIndex + 1);
544         }
545
546
547         /**
548          * Average selected points
549          * @param inStartIndex start index of selection
550          * @param inEndIndex end index of selection
551          * @return true if successful
552          */
553         public boolean average(int inStartIndex, int inEndIndex)
554         {
555                 // check parameters
556                 if (inStartIndex < 0 || inStartIndex >= _numPoints || inEndIndex <= inStartIndex)
557                         return false;
558
559                 DataPoint startPoint = getPoint(inStartIndex);
560                 double firstLatitude = startPoint.getLatitude().getDouble();
561                 double firstLongitude = startPoint.getLongitude().getDouble();
562                 double latitudeDiff = 0.0, longitudeDiff = 0.0;
563                 double totalAltitude = 0;
564                 int numAltitudes = 0;
565                 Unit altUnit = null;
566                 // loop between start and end points
567                 for (int i=inStartIndex; i<= inEndIndex; i++)
568                 {
569                         DataPoint currPoint = getPoint(i);
570                         latitudeDiff += (currPoint.getLatitude().getDouble() - firstLatitude);
571                         longitudeDiff += (currPoint.getLongitude().getDouble() - firstLongitude);
572                         if (currPoint.hasAltitude())
573                         {
574                                 totalAltitude += currPoint.getAltitude().getValue(altUnit);
575                                 // Use altitude format of first valid altitude
576                                 if (altUnit == null)
577                                         altUnit = currPoint.getAltitude().getUnit();
578                                 numAltitudes++;
579                         }
580                 }
581                 int numPoints = inEndIndex - inStartIndex + 1;
582                 double meanLatitude = firstLatitude + (latitudeDiff / numPoints);
583                 double meanLongitude = firstLongitude + (longitudeDiff / numPoints);
584                 Altitude meanAltitude = null;
585                 if (numAltitudes > 0) {
586                         meanAltitude = new Altitude((int) (totalAltitude / numAltitudes), altUnit);
587                 }
588
589                 DataPoint insertedPoint = new DataPoint(new Latitude(meanLatitude, Coordinate.FORMAT_DECIMAL_FORCE_POINT),
590                         new Longitude(meanLongitude, Coordinate.FORMAT_DECIMAL_FORCE_POINT), meanAltitude);
591                 // Make into singleton
592                 insertedPoint.setSegmentStart(true);
593                 DataPoint nextPoint = getNextTrackPoint(inEndIndex+1);
594                 if (nextPoint != null) {nextPoint.setSegmentStart(true);}
595                 // Insert points into track
596                 return insertRange(new DataPoint[] {insertedPoint}, inEndIndex + 1);
597         }
598
599
600         /**
601          * Append the specified points to the end of the track
602          * @param inPoints DataPoint objects to add
603          */
604         public void appendPoints(DataPoint[] inPoints)
605         {
606                 // Insert points into track
607                 if (inPoints != null && inPoints.length > 0)
608                 {
609                         insertRange(inPoints, _numPoints);
610                 }
611                 // needs to be scaled again to recalc x, y
612                 _scaled = false;
613                 UpdateMessageBroker.informSubscribers();
614         }
615
616
617         //////// information methods /////////////
618
619
620         /**
621          * Get the point at the given index
622          * @param inPointNum index number, starting at 0
623          * @return DataPoint object, or null if out of range
624          */
625         public DataPoint getPoint(int inPointNum)
626         {
627                 if (inPointNum > -1 && inPointNum < getNumPoints())
628                 {
629                         return _dataPoints[inPointNum];
630                 }
631                 return null;
632         }
633
634         /**
635          * @return the number of (valid) points in the track
636          */
637         public int getNumPoints()
638         {
639                 return _numPoints;
640         }
641
642         /**
643          * @return The range of x values as a DoubleRange object
644          */
645         public DoubleRange getXRange()
646         {
647                 if (!_scaled) {scalePoints();}
648                 return _xRange;
649         }
650
651         /**
652          * @return The range of y values as a DoubleRange object
653          */
654         public DoubleRange getYRange()
655         {
656                 if (!_scaled) {scalePoints();}
657                 return _yRange;
658         }
659
660         /**
661          * @return The range of lat values as a DoubleRange object
662          */
663         public DoubleRange getLatRange()
664         {
665                 if (!_scaled) {scalePoints();}
666                 return _latRange;
667         }
668         /**
669          * @return The range of lon values as a DoubleRange object
670          */
671         public DoubleRange getLonRange()
672         {
673                 if (!_scaled) {scalePoints();}
674                 return _longRange;
675         }
676
677         /**
678          * @param inPointNum point index, starting at 0
679          * @return scaled x value of specified point
680          */
681         public double getX(int inPointNum)
682         {
683                 if (!_scaled) {scalePoints();}
684                 return _xValues[inPointNum];
685         }
686
687         /**
688          * @param inPointNum point index, starting at 0
689          * @return scaled y value of specified point
690          */
691         public double getY(int inPointNum)
692         {
693                 if (!_scaled) {scalePoints();}
694                 return _yValues[inPointNum];
695         }
696
697         /**
698          * @return the master field list
699          */
700         public FieldList getFieldList()
701         {
702                 return _masterFieldList;
703         }
704
705
706         /**
707          * Checks if any data exists for the specified field
708          * @param inField Field to examine
709          * @return true if data exists for this field
710          */
711         public boolean hasData(Field inField)
712         {
713                 // Don't use this method for altitudes
714                 if (inField.equals(Field.ALTITUDE)) {return hasAltitudeData();}
715                 return hasData(inField, 0, _numPoints-1);
716         }
717
718
719         /**
720          * Checks if any data exists for the specified field in the specified range
721          * @param inField Field to examine
722          * @param inStart start of range to check
723          * @param inEnd end of range to check (inclusive)
724          * @return true if data exists for this field
725          */
726         public boolean hasData(Field inField, int inStart, int inEnd)
727         {
728                 // Loop over selected point range
729                 for (int i=inStart; i<=inEnd; i++)
730                 {
731                         if (_dataPoints[i].getFieldValue(inField) != null)
732                         {
733                                 // Check altitudes and timestamps
734                                 if ((inField != Field.ALTITUDE || _dataPoints[i].getAltitude().isValid())
735                                         && (inField != Field.TIMESTAMP || _dataPoints[i].getTimestamp().isValid()))
736                                 {
737                                         return true;
738                                 }
739                         }
740                 }
741                 return false;
742         }
743
744         /**
745          * @return true if track has altitude data
746          */
747         public boolean hasAltitudeData()
748         {
749                 for (int i=0; i<_numPoints; i++) {
750                         if (_dataPoints[i].hasAltitude()) {return true;}
751                 }
752                 return false;
753         }
754
755         /**
756          * @return true if track contains at least one trackpoint
757          */
758         public boolean hasTrackPoints()
759         {
760                 if (!_scaled) {scalePoints();}
761                 return _hasTrackpoint;
762         }
763
764         /**
765          * @return true if track contains waypoints
766          */
767         public boolean hasWaypoints()
768         {
769                 if (!_scaled) {scalePoints();}
770                 return _hasWaypoint;
771         }
772
773         /**
774          * @return true if track contains any points marked for deletion
775          */
776         public boolean hasMarkedPoints()
777         {
778                 if (_numPoints < 1) {
779                         return false;
780                 }
781                 // Loop over points looking for any marked for deletion
782                 for (int i=0; i<=_numPoints-1; i++)
783                 {
784                         if (_dataPoints[i] != null && _dataPoints[i].getDeleteFlag()) {
785                                 return true;
786                         }
787                 }
788                 // None found
789                 return false;
790         }
791
792         /**
793          * Clear all the deletion markers
794          */
795         public void clearDeletionMarkers()
796         {
797                 for (int i=0; i<_numPoints; i++)
798                 {
799                         _dataPoints[i].setMarkedForDeletion(false);
800                 }
801         }
802
803         /**
804          * Collect all the waypoints into the given List
805          * @param inList List to fill with waypoints
806          */
807         public void getWaypoints(List<DataPoint> inList)
808         {
809                 // clear list
810                 inList.clear();
811                 // loop over points and copy all waypoints into list
812                 for (int i=0; i<=_numPoints-1; i++)
813                 {
814                         if (_dataPoints[i] != null && _dataPoints[i].isWaypoint())
815                         {
816                                 inList.add(_dataPoints[i]);
817                         }
818                 }
819         }
820
821         /**
822          * Collect all segment starts into the given list
823          * @param inList List to fill with waypoints
824          */
825         public void getSegmentStarts(List<DataPoint> inList)
826         {
827                 // clear list
828                 inList.clear();
829                 // loop over points and copy all segment starts into list
830                 for (int i=0; i<=_numPoints-1; i++)
831                 {
832                         if (_dataPoints[i] != null && _dataPoints[i].getSegmentStart())
833                         {
834                                 inList.add(_dataPoints[i]);
835                         }
836                 }
837         }
838
839
840         /**
841          * Search for the given Point in the track and return the index
842          * @param inPoint Point to look for
843          * @return index of Point, if any or -1 if not found
844          */
845         public int getPointIndex(DataPoint inPoint)
846         {
847                 if (inPoint != null)
848                 {
849                         // Loop over points in track
850                         for (int i=0; i<=_numPoints-1; i++)
851                         {
852                                 if (_dataPoints[i] == inPoint)
853                                 {
854                                         return i;
855                                 }
856                         }
857                 }
858                 // not found
859                 return -1;
860         }
861
862
863         ///////// Internal processing methods ////////////////
864
865
866         /**
867          * Scale all the points in the track to gain x and y values
868          * ready for plotting
869          */
870         private synchronized void scalePoints()
871         {
872                 // Loop through all points in track, to see limits of lat, long
873                 _longRange = new DoubleRange();
874                 _latRange = new DoubleRange();
875                 int p;
876                 _hasWaypoint = false; _hasTrackpoint = false;
877                 for (p=0; p < getNumPoints(); p++)
878                 {
879                         DataPoint point = getPoint(p);
880                         if (point != null && point.isValid())
881                         {
882                                 _longRange.addValue(point.getLongitude().getDouble());
883                                 _latRange.addValue(point.getLatitude().getDouble());
884                                 if (point.isWaypoint())
885                                         _hasWaypoint = true;
886                                 else
887                                         _hasTrackpoint = true;
888                         }
889                 }
890
891                 // Loop over points and calculate scales
892                 _xValues = new double[getNumPoints()];
893                 _yValues = new double[getNumPoints()];
894                 _xRange = new DoubleRange();
895                 _yRange = new DoubleRange();
896                 for (p=0; p < getNumPoints(); p++)
897                 {
898                         DataPoint point = getPoint(p);
899                         if (point != null)
900                         {
901                                 _xValues[p] = MapUtils.getXFromLongitude(point.getLongitude().getDouble());
902                                 _xRange.addValue(_xValues[p]);
903                                 _yValues[p] = MapUtils.getYFromLatitude(point.getLatitude().getDouble());
904                                 _yRange.addValue(_yValues[p]);
905                         }
906                 }
907                 _scaled = true;
908         }
909
910
911         /**
912          * Find the nearest point to the specified x and y coordinates
913          * or -1 if no point is within the specified max distance
914          * @param inX x coordinate
915          * @param inY y coordinate
916          * @param inMaxDist maximum distance from selected coordinates
917          * @param inJustTrackPoints true if waypoints should be ignored
918          * @return index of nearest point or -1 if not found
919          */
920         public int getNearestPointIndex(double inX, double inY, double inMaxDist, boolean inJustTrackPoints)
921         {
922                 int nearestPoint = 0;
923                 double nearestDist = -1.0;
924                 double mDist, yDist;
925                 for (int i=0; i < getNumPoints(); i++)
926                 {
927                         if (!inJustTrackPoints || !_dataPoints[i].isWaypoint())
928                         {
929                                 yDist = Math.abs(_yValues[i] - inY);
930                                 if (yDist < nearestDist || nearestDist < 0.0)
931                                 {
932                                         // y dist is within range, so check x too
933                                         mDist = yDist + getMinXDist(_xValues[i] - inX);
934                                         if (mDist < nearestDist || nearestDist < 0.0)
935                                         {
936                                                 nearestPoint = i;
937                                                 nearestDist = mDist;
938                                         }
939                                 }
940                         }
941                 }
942                 // Check whether it's within required distance
943                 if (nearestDist > inMaxDist && inMaxDist > 0.0)
944                 {
945                         return -1;
946                 }
947                 return nearestPoint;
948         }
949
950         /**
951          * @param inX x value of point
952          * @return minimum wrapped value
953          */
954         private static final double getMinXDist(double inX)
955         {
956                 // 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
957                 return Math.min(Math.min(Math.abs(inX), Math.abs(inX-1.0)), Math.abs(inX+1.0));
958         }
959
960         /**
961          * Get the next track point starting from the given index
962          * @param inStartIndex index to start looking from
963          * @return next track point, or null if end of data reached
964          */
965         public DataPoint getNextTrackPoint(int inStartIndex)
966         {
967                 return getNextTrackPoint(inStartIndex, _numPoints, true);
968         }
969
970         /**
971          * Get the next track point in the given range
972          * @param inStartIndex index to start looking from
973          * @param inEndIndex index to stop looking
974          * @return next track point, or null if end of data reached
975          */
976         public DataPoint getNextTrackPoint(int inStartIndex, int inEndIndex)
977         {
978                 return getNextTrackPoint(inStartIndex, inEndIndex, true);
979         }
980
981         /**
982          * Get the previous track point starting from the given index
983          * @param inStartIndex index to start looking from
984          * @return next track point, or null if end of data reached
985          */
986         public DataPoint getPreviousTrackPoint(int inStartIndex)
987         {
988                 // end index is given as _numPoints but actually it just counts down to -1
989                 return getNextTrackPoint(inStartIndex, _numPoints, false);
990         }
991
992         /**
993          * Get the next track point starting from the given index
994          * @param inStartIndex index to start looking from
995          * @param inEndIndex index to stop looking (inclusive)
996          * @param inCountUp true for next, false for previous
997          * @return next track point, or null if end of data reached
998          */
999         private DataPoint getNextTrackPoint(int inStartIndex, int inEndIndex, boolean inCountUp)
1000         {
1001                 // Loop forever over points
1002                 int increment = inCountUp?1:-1;
1003                 for (int i=inStartIndex; i<=inEndIndex; i+=increment)
1004                 {
1005                         DataPoint point = getPoint(i);
1006                         // Exit if end of data reached - there wasn't a track point
1007                         if (point == null) {return null;}
1008                         if (point.isValid() && !point.isWaypoint()) {
1009                                 // next track point found
1010                                 return point;
1011                         }
1012                 }
1013                 return null;
1014         }
1015
1016         /**
1017          * Shift all the segment start flags in the given range by 1
1018          * Method used by reverse range and its undo
1019          * @param inStartIndex start of range, inclusive
1020          * @param inEndIndex end of range, inclusive
1021          */
1022         public void shiftSegmentStarts(int inStartIndex, int inEndIndex)
1023         {
1024                 boolean prevFlag = true;
1025                 boolean currFlag = true;
1026                 for (int i=inStartIndex; i<= inEndIndex; i++)
1027                 {
1028                         DataPoint point = getPoint(i);
1029                         if (point != null && !point.isWaypoint())
1030                         {
1031                                 // remember flag
1032                                 currFlag = point.getSegmentStart();
1033                                 // shift flag by 1
1034                                 point.setSegmentStart(prevFlag);
1035                                 prevFlag = currFlag;
1036                         }
1037                 }
1038         }
1039
1040         ////////////////// Cloning and replacing ///////////////////
1041
1042         /**
1043          * Clone the array of DataPoints
1044          * @return shallow copy of DataPoint objects
1045          */
1046         public DataPoint[] cloneContents()
1047         {
1048                 DataPoint[] clone = new DataPoint[getNumPoints()];
1049                 System.arraycopy(_dataPoints, 0, clone, 0, getNumPoints());
1050                 return clone;
1051         }
1052
1053
1054         /**
1055          * Clone the specified range of data points
1056          * @param inStart start index (inclusive)
1057          * @param inEnd end index (inclusive)
1058          * @return shallow copy of DataPoint objects
1059          */
1060         public DataPoint[] cloneRange(int inStart, int inEnd)
1061         {
1062                 int numSelected = 0;
1063                 if (inEnd >= 0 && inEnd >= inStart)
1064                 {
1065                         numSelected = inEnd - inStart + 1;
1066                 }
1067                 DataPoint[] result = new DataPoint[numSelected>0?numSelected:0];
1068                 if (numSelected > 0)
1069                 {
1070                         System.arraycopy(_dataPoints, inStart, result, 0, numSelected);
1071                 }
1072                 return result;
1073         }
1074
1075
1076         /**
1077          * Re-insert the specified point at the given index
1078          * @param inPoint point to insert
1079          * @param inIndex index at which to insert the point
1080          * @return true if it worked, false otherwise
1081          */
1082         public boolean insertPoint(DataPoint inPoint, int inIndex)
1083         {
1084                 if (inIndex > _numPoints || inPoint == null)
1085                 {
1086                         return false;
1087                 }
1088                 // Make new array to copy points over to
1089                 DataPoint[] newPointArray = new DataPoint[_numPoints + 1];
1090                 if (inIndex > 0)
1091                 {
1092                         System.arraycopy(_dataPoints, 0, newPointArray, 0, inIndex);
1093                 }
1094                 newPointArray[inIndex] = inPoint;
1095                 if (inIndex < _numPoints)
1096                 {
1097                         System.arraycopy(_dataPoints, inIndex, newPointArray, inIndex+1, _numPoints - inIndex);
1098                 }
1099                 // Change over to new array
1100                 _dataPoints = newPointArray;
1101                 _numPoints++;
1102                 // needs to be scaled again
1103                 _scaled = false;
1104                 UpdateMessageBroker.informSubscribers();
1105                 return true;
1106         }
1107
1108
1109         /**
1110          * Re-insert the specified point range at the given index
1111          * @param inPoints point array to insert
1112          * @param inIndex index at which to insert the points
1113          * @return true if it worked, false otherwise
1114          */
1115         public boolean insertRange(DataPoint[] inPoints, int inIndex)
1116         {
1117                 if (inIndex > _numPoints || inPoints == null)
1118                 {
1119                         return false;
1120                 }
1121                 // Make new array to copy points over to
1122                 DataPoint[] newPointArray = new DataPoint[_numPoints + inPoints.length];
1123                 if (inIndex > 0)
1124                 {
1125                         System.arraycopy(_dataPoints, 0, newPointArray, 0, inIndex);
1126                 }
1127                 System.arraycopy(inPoints, 0, newPointArray, inIndex, inPoints.length);
1128                 if (inIndex < _numPoints)
1129                 {
1130                         System.arraycopy(_dataPoints, inIndex, newPointArray, inIndex+inPoints.length, _numPoints - inIndex);
1131                 }
1132                 // Change over to new array
1133                 _dataPoints = newPointArray;
1134                 _numPoints += inPoints.length;
1135                 // needs to be scaled again
1136                 _scaled = false;
1137                 UpdateMessageBroker.informSubscribers();
1138                 return true;
1139         }
1140
1141
1142         /**
1143          * Replace the track contents with the given point array
1144          * @param inContents array of DataPoint objects
1145          * @return true on success
1146          */
1147         public boolean replaceContents(DataPoint[] inContents)
1148         {
1149                 // master field array stays the same
1150                 // (would need to store field array too if we wanted to redo a load)
1151                 // replace data array
1152                 _dataPoints = inContents;
1153                 _numPoints = _dataPoints.length;
1154                 _scaled = false;
1155                 UpdateMessageBroker.informSubscribers();
1156                 return true;
1157         }
1158
1159
1160         /**
1161          * Edit the specified point
1162          * @param inPoint point to edit
1163          * @param inEditList list of edits to make
1164          * @param inUndo true if undo operation, false otherwise
1165          * @return true if successful
1166          */
1167         public boolean editPoint(DataPoint inPoint, FieldEditList inEditList, boolean inUndo)
1168         {
1169                 if (inPoint != null && inEditList != null && inEditList.getNumEdits() > 0)
1170                 {
1171                         // remember if coordinates have changed
1172                         boolean coordsChanged = false;
1173                         // go through edits one by one
1174                         int numEdits = inEditList.getNumEdits();
1175                         for (int i=0; i<numEdits; i++)
1176                         {
1177                                 FieldEdit edit = inEditList.getEdit(i);
1178                                 Field editField = edit.getField();
1179                                 inPoint.setFieldValue(editField, edit.getValue(), inUndo);
1180                                 // Check that master field list has this field already (maybe point name has been added)
1181                                 if (!_masterFieldList.contains(editField)) {
1182                                         _masterFieldList.extendList(editField);
1183                                 }
1184                                 // check coordinates
1185                                 coordsChanged |= (editField.equals(Field.LATITUDE)
1186                                         || editField.equals(Field.LONGITUDE) || editField.equals(Field.ALTITUDE));
1187                         }
1188                         // set photo status if coordinates have changed
1189                         if (inPoint.getPhoto() != null && coordsChanged)
1190                         {
1191                                 inPoint.getPhoto().setCurrentStatus(Photo.Status.CONNECTED);
1192                         }
1193                         // point possibly needs to be scaled again
1194                         _scaled = false;
1195                         // trigger listeners
1196                         UpdateMessageBroker.informSubscribers();
1197                         return true;
1198                 }
1199                 return false;
1200         }
1201
1202         /**
1203          * @param inPoint point to check
1204          * @return true if this track contains the given point
1205          */
1206         public boolean containsPoint(DataPoint inPoint)
1207         {
1208                 if (inPoint == null) return false;
1209                 for (int i=0; i < getNumPoints(); i++)
1210                 {
1211                         if (getPoint(i) == inPoint) return true;
1212                 }
1213                 return false; // not found
1214         }
1215 }