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