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