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