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