]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/correlate/PhotoCorrelator.java
Version 6, October 2008
[GpsPrune.git] / tim / prune / correlate / PhotoCorrelator.java
1 package tim.prune.correlate;
2
3 import java.awt.BorderLayout;
4 import java.awt.CardLayout;
5 import java.awt.Component;
6 import java.awt.Dimension;
7 import java.awt.FlowLayout;
8 import java.awt.event.ActionEvent;
9 import java.awt.event.ActionListener;
10 import java.util.Calendar;
11 import java.util.Iterator;
12 import java.util.TreeSet;
13
14 import javax.swing.BorderFactory;
15 import javax.swing.BoxLayout;
16 import javax.swing.ButtonGroup;
17 import javax.swing.JButton;
18 import javax.swing.JComboBox;
19 import javax.swing.JDialog;
20 import javax.swing.JFrame;
21 import javax.swing.JLabel;
22 import javax.swing.JOptionPane;
23 import javax.swing.JPanel;
24 import javax.swing.JRadioButton;
25 import javax.swing.JScrollPane;
26 import javax.swing.JTable;
27 import javax.swing.JTextField;
28
29 import tim.prune.App;
30 import tim.prune.I18nManager;
31 import tim.prune.data.DataPoint;
32 import tim.prune.data.Distance;
33 import tim.prune.data.Field;
34 import tim.prune.data.Photo;
35 import tim.prune.data.PhotoList;
36 import tim.prune.data.TimeDifference;
37 import tim.prune.data.Timestamp;
38 import tim.prune.data.Track;
39 import tim.prune.data.TrackInfo;
40
41 /**
42  * Class to manage the automatic correlation of photos to points
43  * including the GUI stuff to control the correlation options
44  */
45 public class PhotoCorrelator
46 {
47         private App _app;
48         private JFrame _parentFrame;
49         private JDialog _dialog;
50         private JButton _nextButton = null, _backButton = null;
51         private JButton _okButton = null;
52         private JPanel _cards = null;
53         private JTable _photoSelectionTable = null;
54         private JLabel _tipLabel = null;
55         private JTextField _offsetHourBox = null, _offsetMinBox = null, _offsetSecBox = null;
56         private JRadioButton _photoLaterOption = null, _pointLaterOption = null;
57         private JRadioButton _timeLimitRadio = null, _distLimitRadio = null;
58         private JTextField _limitMinBox = null, _limitSecBox = null;
59         private JTextField _limitDistBox = null;
60         private JComboBox _distUnitsDropdown = null;
61         private JTable _previewTable = null;
62         private boolean _firstTabAvailable = false;
63         private boolean _previewEnabled = false; // flag required to enable preview function on second panel
64
65
66         /**
67          * Constructor
68          * @param inApp App object to report actions to
69          * @param inFrame parent frame for dialogs
70          */
71         public PhotoCorrelator(App inApp, JFrame inFrame)
72         {
73                 _app = inApp;
74                 _parentFrame = inFrame;
75                 _dialog = new JDialog(inFrame, I18nManager.getText("dialog.correlate.title"), true);
76                 _dialog.setLocationRelativeTo(inFrame);
77                 _dialog.getContentPane().add(makeDialogContents());
78                 _dialog.pack();
79         }
80
81
82         /**
83          * Reset dialog and show it
84          */
85         public void begin()
86         {
87                 // Check whether track has timestamps, exit if not
88                 if (!_app.getTrackInfo().getTrack().hasData(Field.TIMESTAMP))
89                 {
90                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.correlate.notimestamps"),
91                                 I18nManager.getText("dialog.correlate.title"), JOptionPane.INFORMATION_MESSAGE);
92                         return;
93                 }
94                 // Check for any non-correlated photos, show warning continue/cancel
95                 if (!trackHasUncorrelatedPhotos())
96                 {
97                         Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")};
98                         if (JOptionPane.showOptionDialog(_parentFrame, I18nManager.getText("dialog.correlate.nouncorrelatedphotos"),
99                                         I18nManager.getText("dialog.correlate.title"), JOptionPane.YES_NO_OPTION,
100                                         JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
101                                 == JOptionPane.NO_OPTION)
102                         {
103                                 return;
104                         }
105                 }
106                 PhotoSelectionTableModel model = makePhotoSelectionTableModel(_app.getTrackInfo());
107                 _firstTabAvailable = model != null && model.getRowCount() > 0;
108                 CardLayout cl = (CardLayout) _cards.getLayout();
109                 if (_firstTabAvailable)
110                 {
111                         cl.first(_cards);
112                         _nextButton.setEnabled(true);
113                         _backButton.setEnabled(false);
114                         _tipLabel.setVisible(false);
115                         _photoSelectionTable.setModel(model);
116                         _previewEnabled = false;
117                         for (int i=0; i<model.getColumnCount(); i++) {
118                                 _photoSelectionTable.getColumnModel().getColumn(i).setPreferredWidth(i==3?50:150);
119                         }
120                         // Calculate median time difference, select corresponding row of table
121                         int preselectedIndex = model.getRowCount() < 3 ? 0 : getMedianIndex(model);
122                         _photoSelectionTable.getSelectionModel().setSelectionInterval(preselectedIndex, preselectedIndex);
123                         _nextButton.requestFocus();
124                 }
125                 else
126                 {
127                         _tipLabel.setVisible(true);
128                         setupSecondCard(null);
129                 }
130                 _dialog.show();
131         }
132
133
134         /**
135          * Make contents of correlate dialog
136          * @return JPanel containing gui elements
137          */
138         private JPanel makeDialogContents()
139         {
140                 JPanel mainPanel = new JPanel();
141                 mainPanel.setLayout(new BorderLayout());
142                 // Card panel in the middle
143                 _cards = new JPanel();
144                 _cards.setLayout(new CardLayout());
145
146                 // First panel for photo selection table
147                 JPanel card1 = new JPanel();
148                 card1.setLayout(new BorderLayout(10, 10));
149                 card1.add(new JLabel(I18nManager.getText("dialog.correlate.photoselect.intro")), BorderLayout.NORTH);
150                 _photoSelectionTable = new JTable();
151                 JScrollPane photoScrollPane = new JScrollPane(_photoSelectionTable);
152                 photoScrollPane.setPreferredSize(new Dimension(400, 100));
153                 card1.add(photoScrollPane, BorderLayout.CENTER);
154                 _cards.add(card1, "card1");
155
156                 OptionsChangedListener optionsChangedListener = new OptionsChangedListener(this);
157                 // Second panel for options
158                 JPanel card2 = new JPanel();
159                 card2.setLayout(new BorderLayout());
160                 JPanel card2Top = new JPanel();
161                 card2Top.setLayout(new BoxLayout(card2Top, BoxLayout.Y_AXIS));
162                 _tipLabel = new JLabel(I18nManager.getText("dialog.correlate.options.tip"));
163                 card2Top.add(_tipLabel);
164                 card2Top.add(new JLabel(I18nManager.getText("dialog.correlate.options.intro")));
165                 // time offset section
166                 JPanel offsetPanel = new JPanel();
167                 offsetPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.offsetpanel")));
168                 offsetPanel.setLayout(new BoxLayout(offsetPanel, BoxLayout.Y_AXIS));
169                 JPanel offsetPanelTop = new JPanel();
170                 offsetPanelTop.setLayout(new FlowLayout());
171                 offsetPanelTop.setBorder(null);
172                 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset") + ": "));
173                 _offsetHourBox = new JTextField(3);
174                 _offsetHourBox.addKeyListener(optionsChangedListener);
175                 offsetPanelTop.add(_offsetHourBox);
176                 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.hours")));
177                 _offsetMinBox = new JTextField(3);
178                 _offsetMinBox.addKeyListener(optionsChangedListener);
179                 offsetPanelTop.add(_offsetMinBox);
180                 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
181                 _offsetSecBox = new JTextField(3);
182                 _offsetSecBox.addKeyListener(optionsChangedListener);
183                 offsetPanelTop.add(_offsetSecBox);
184                 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
185                 offsetPanel.add(offsetPanelTop);
186
187                 // radio buttons for photo / point later
188                 JPanel offsetPanelBot = new JPanel();
189                 offsetPanelBot.setLayout(new FlowLayout());
190                 offsetPanelBot.setBorder(null);
191                 _photoLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.photolater"));
192                 _pointLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.pointlater"));
193                 _photoLaterOption.addItemListener(optionsChangedListener);
194                 _pointLaterOption.addItemListener(optionsChangedListener);
195                 ButtonGroup laterGroup = new ButtonGroup();
196                 laterGroup.add(_photoLaterOption);
197                 laterGroup.add(_pointLaterOption);
198                 offsetPanelBot.add(_photoLaterOption);
199                 offsetPanelBot.add(_pointLaterOption);
200                 offsetPanel.add(offsetPanelBot);
201                 offsetPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
202                 card2Top.add(offsetPanel);
203
204                 // time limits section
205                 JPanel limitsPanel = new JPanel();
206                 limitsPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.limitspanel")));
207                 limitsPanel.setLayout(new BoxLayout(limitsPanel, BoxLayout.Y_AXIS));
208                 JPanel timeLimitPanel = new JPanel();
209                 timeLimitPanel.setLayout(new FlowLayout());
210                 JRadioButton noTimeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.notimelimit"));
211                 noTimeLimitRadio.addItemListener(optionsChangedListener);
212                 timeLimitPanel.add(noTimeLimitRadio);
213                 _timeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.timelimit") + " : ");
214                 _timeLimitRadio.addItemListener(optionsChangedListener);
215                 timeLimitPanel.add(_timeLimitRadio);
216                 groupRadioButtons(noTimeLimitRadio, _timeLimitRadio);
217                 _limitMinBox = new JTextField(3);
218                 _limitMinBox.addKeyListener(optionsChangedListener);
219                 timeLimitPanel.add(_limitMinBox);
220                 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
221                 _limitSecBox = new JTextField(3);
222                 _limitSecBox.addKeyListener(optionsChangedListener);
223                 timeLimitPanel.add(_limitSecBox);
224                 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
225                 limitsPanel.add(timeLimitPanel);
226                 // distance limits
227                 JPanel distLimitPanel = new JPanel();
228                 distLimitPanel.setLayout(new FlowLayout());
229                 JRadioButton noDistLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.nodistancelimit"));
230                 noDistLimitRadio.addItemListener(optionsChangedListener);
231                 distLimitPanel.add(noDistLimitRadio);
232                 _distLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.distancelimit"));
233                 _distLimitRadio.addItemListener(optionsChangedListener);
234                 distLimitPanel.add(_distLimitRadio);
235                 groupRadioButtons(noDistLimitRadio, _distLimitRadio);
236                 _limitDistBox = new JTextField(4);
237                 _limitDistBox.addKeyListener(optionsChangedListener);
238                 distLimitPanel.add(_limitDistBox);
239                 String[] distUnitsOptions = {I18nManager.getText("units.kilometres"), I18nManager.getText("units.metres"),
240                         I18nManager.getText("units.miles")};
241                 _distUnitsDropdown = new JComboBox(distUnitsOptions);
242                 _distUnitsDropdown.addItemListener(optionsChangedListener);
243                 distLimitPanel.add(_distUnitsDropdown);
244                 limitsPanel.add(distLimitPanel);
245                 limitsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
246                 card2Top.add(limitsPanel);
247
248                 // preview button
249                 JButton previewButton = new JButton(I18nManager.getText("button.preview"));
250                 previewButton.addActionListener(new ActionListener() {
251                         public void actionPerformed(ActionEvent e)
252                         {
253                                 createPreview(true);
254                         }
255                 });
256                 card2Top.add(previewButton);
257                 card2.add(card2Top, BorderLayout.NORTH);
258                 // preview
259                 _previewTable = new JTable();
260                 JScrollPane previewScrollPane = new JScrollPane(_previewTable);
261                 previewScrollPane.setPreferredSize(new Dimension(300, 100));
262                 card2.add(previewScrollPane, BorderLayout.CENTER);
263                 _cards.add(card2, "card2");
264                 mainPanel.add(_cards, BorderLayout.CENTER);
265
266                 // Button panel at the bottom
267                 JPanel buttonPanel = new JPanel();
268                 _backButton = new JButton(I18nManager.getText("button.back"));
269                 _backButton.addActionListener(new ActionListener()
270                         {
271                                 public void actionPerformed(ActionEvent e)
272                                 {
273                                         CardLayout cl = (CardLayout) _cards.getLayout();
274                                         cl.previous(_cards);
275                                         _backButton.setEnabled(false);
276                                         _nextButton.setEnabled(true);
277                                         _okButton.setEnabled(false);
278                                         _previewEnabled = false;
279                                 }
280                         });
281                 _backButton.setEnabled(false);
282                 buttonPanel.add(_backButton);
283                 _nextButton = new JButton(I18nManager.getText("button.next"));
284                 _nextButton.addActionListener(new ActionListener()
285                         {
286                                 public void actionPerformed(ActionEvent e)
287                                 {
288                                         int rowNum = _photoSelectionTable.getSelectedRow();
289                                         if (rowNum < 0) {rowNum = 0;}
290                                         PhotoSelectionTableRow selectedRow = ((PhotoSelectionTableModel) _photoSelectionTable.getModel())
291                                                 .getRow(rowNum);
292                                         setupSecondCard(selectedRow.getTimeDiff());
293                                 }
294                         });
295                 buttonPanel.add(_nextButton);
296                 _okButton = new JButton(I18nManager.getText("button.ok"));
297                 _okButton.addActionListener(new ActionListener()
298                         {
299                                 public void actionPerformed(ActionEvent e)
300                                 {
301                                         _app.finishCorrelatePhotos(getPointPairs());
302                                         _dialog.dispose();
303                                 }
304                         });
305                 _okButton.setEnabled(false);
306                 buttonPanel.add(_okButton);
307                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
308                 cancelButton.addActionListener(new ActionListener()
309                         {
310                                 public void actionPerformed(ActionEvent e)
311                                 {
312                                         _dialog.dispose();
313                                 }
314                         });
315                 buttonPanel.add(cancelButton);
316                 mainPanel.add(buttonPanel, BorderLayout.SOUTH);
317                 return mainPanel;
318         }
319
320
321         /**
322          * Construct a table model for the photo selection table
323          * @param inTrackInfo track info object
324          * @return table model
325          */
326         private static PhotoSelectionTableModel makePhotoSelectionTableModel(TrackInfo inTrackInfo)
327         {
328                 PhotoSelectionTableModel model = new PhotoSelectionTableModel();
329                 int numPhotos = inTrackInfo.getPhotoList().getNumPhotos();
330                 for (int i=0; i<numPhotos; i++)
331                 {
332                         Photo photo = inTrackInfo.getPhotoList().getPhoto(i);
333                         if (photo.getDataPoint() != null && photo.getDataPoint().hasTimestamp())
334                         {
335                                 // Calculate time difference, add to table model
336                                 long timeDiff = photo.getTimestamp().getSecondsSince(photo.getDataPoint().getTimestamp());
337                                 model.addPhoto(photo, timeDiff);
338                         }
339                 }
340                 return model;
341         }
342
343
344         /**
345          * Group the two radio buttons together with a ButtonGroup
346          * @param inButton1 first radio button
347          * @param inButton2 second radio button
348          */
349         private static void groupRadioButtons(JRadioButton inButton1, JRadioButton inButton2)
350         {
351                 ButtonGroup buttonGroup = new ButtonGroup();
352                 buttonGroup.add(inButton1);
353                 buttonGroup.add(inButton2);
354                 inButton1.setSelected(true);
355         }
356
357
358         /**
359          * Set up the second card using the given time difference and show it
360          * @param inTimeDiff time difference to use for photo time offsets
361          */
362         private void setupSecondCard(TimeDifference inTimeDiff)
363         {
364                 _previewEnabled = false;
365                 boolean hasTimeDiff = inTimeDiff != null;
366                 if (!hasTimeDiff)
367                 {
368                         // No time difference available, so calculate based on computer's time zone
369                         inTimeDiff = getTimezoneOffset();
370                 }
371                 // Use time difference to set edit boxes
372                 _offsetHourBox.setText("" + inTimeDiff.getNumHours());
373                 _offsetMinBox.setText("" + inTimeDiff.getNumMinutes());
374                 _offsetSecBox.setText("" + inTimeDiff.getNumSeconds());
375                 _photoLaterOption.setSelected(inTimeDiff.getIsPositive());
376                 _pointLaterOption.setSelected(!inTimeDiff.getIsPositive());
377                 createPreview(inTimeDiff, true);
378                 CardLayout cl = (CardLayout) _cards.getLayout();
379                 cl.next(_cards);
380                 _backButton.setEnabled(hasTimeDiff);
381                 _nextButton.setEnabled(false);
382                 // enable ok button if any photos have been selected
383                 _okButton.setEnabled(((PhotoPreviewTableModel) _previewTable.getModel()).hasPhotosSelected());
384                 _previewEnabled = true;
385         }
386
387
388         /**
389          * Create a preview of the correlate action using the selected time difference
390          * @param inFromButton true if triggered from button press, false if automatic
391          */
392         public void createPreview(boolean inFromButton)
393         {
394                 // Exit if still on first panel
395                 if (!_previewEnabled) {return;}
396                 // Create a TimeDifference based on the edit boxes
397                 int numHours = getValue(_offsetHourBox.getText());
398                 int numMins = getValue(_offsetMinBox.getText());
399                 int numSecs = getValue(_offsetSecBox.getText());
400                 boolean isPos = _photoLaterOption.isSelected();
401                 createPreview(new TimeDifference(numHours, numMins, numSecs, isPos), inFromButton);
402         }
403
404
405         /**
406          * Create a preview of the correlate action using the selected time difference
407          * @param inTimeDiff TimeDifference to use for preview
408          * @param inShowWarning true to show warning if all points out of range
409          */
410         private void createPreview(TimeDifference inTimeDiff, boolean inShowWarning)
411         {
412                 TimeDifference timeLimit = parseTimeLimit();
413                 double angDistLimit = parseDistanceLimit();
414                 PhotoPreviewTableModel model = new PhotoPreviewTableModel();
415                 PhotoList photos = _app.getTrackInfo().getPhotoList();
416                 // Loop through photos deciding whether to set correlate flag or not
417                 int numPhotos = photos.getNumPhotos();
418                 for (int i=0; i<numPhotos; i++)
419                 {
420                         Photo photo = photos.getPhoto(i);
421                         PointPair pair = getPointPairForPhoto(_app.getTrackInfo().getTrack(), photo, inTimeDiff);
422                         PhotoPreviewTableRow row = new PhotoPreviewTableRow(pair);
423                         // Don't try to correlate photos which don't have points either side
424                         boolean correlatePhoto = pair.isValid();
425                         // Check time limits, distance limits
426                         if (timeLimit != null && correlatePhoto) {
427                                 long numSecs = pair.getMinSeconds();
428                                 correlatePhoto = (numSecs <= timeLimit.getTotalSeconds());
429                         }
430                         if (angDistLimit > 0.0 && correlatePhoto)
431                         {
432                                 final double angDistPair = DataPoint.calculateRadiansBetween(pair.getPointBefore(), pair.getPointAfter());
433                                 double frac = pair.getFraction();
434                                 if (frac > 0.5) {frac = 1 - frac;}
435                                 final double angDistPhoto = angDistPair * frac;
436                                 correlatePhoto = (angDistPhoto < angDistLimit);
437                         }
438                         // Don't select photos which are already correlated to the same point
439                         if (pair.getSecondsBefore() == 0L && pair.getPointBefore().getPhoto() != null
440                                 && pair.getPointBefore().getPhoto().equals(photo)) {
441                                 correlatePhoto = false;
442                         }
443                         row.setCorrelateFlag(correlatePhoto);
444                         model.addPhotoRow(row);
445                 }
446                 _previewTable.setModel(model);
447                 // Set distance units
448                 model.setDistanceUnits(getSelectedDistanceUnits());
449                 // Set column widths
450                 _previewTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
451                 final int[] colWidths = {150, 160, 100, 100, 50};
452                 for (int i=0; i<model.getColumnCount(); i++) {
453                         _previewTable.getColumnModel().getColumn(i).setPreferredWidth(colWidths[i]);
454                 }
455                 // check if any photos found
456                 _okButton.setEnabled(model.hasPhotosSelected());
457                 if (inShowWarning && !model.hasPhotosSelected())
458                 {
459                         JOptionPane.showMessageDialog(_dialog, I18nManager.getText("dialog.correlate.alloutsiderange"),
460                                 I18nManager.getText("dialog.correlate.title"), JOptionPane.ERROR_MESSAGE);
461                 }
462         }
463
464         /**
465          * Parse the time limit values entered and validate them
466          * @return TimeDifference object describing limit
467          */
468         private TimeDifference parseTimeLimit()
469         {
470                 if (!_timeLimitRadio.isSelected()) {return null;}
471                 int mins = getValue(_limitMinBox.getText());
472                 _limitMinBox.setText("" + mins);
473                 int secs = getValue(_limitSecBox.getText());
474                 _limitSecBox.setText("" + secs);
475                 if (mins <= 0 && secs <= 0) {return null;}
476                 return new TimeDifference(0, mins, secs, true);
477         }
478
479         /**
480          * Parse the distance limit value entered and validate
481          * @return angular distance in radians
482          */
483         private double parseDistanceLimit()
484         {
485                 double value = -1.0;
486                 if (_distLimitRadio.isSelected())
487                 {
488                         try
489                         {
490                                 value = Double.parseDouble(_limitDistBox.getText());
491                         }
492                         catch (NumberFormatException nfe) {}
493                 }
494                 if (value <= 0.0) {
495                         _limitDistBox.setText("0");
496                         return -1.0;
497                 }
498                 _limitDistBox.setText("" + value);
499                 return Distance.convertDistanceToRadians(value, getSelectedDistanceUnits());
500         }
501
502
503         /**
504          * @return the selected distance units from the dropdown
505          */
506         private int getSelectedDistanceUnits()
507         {
508                 final int[] distUnits = {Distance.UNITS_KILOMETRES, Distance.UNITS_METRES, Distance.UNITS_MILES};
509                 return distUnits[_distUnitsDropdown.getSelectedIndex()];
510         }
511
512
513         /**
514          * Try to parse the given string
515          * @param inText String to parse
516          * @return value if parseable, 0 otherwise
517          */
518         private static int getValue(String inText)
519         {
520                 int value = 0;
521                 try {
522                         value = Integer.parseInt(inText);
523                 }
524                 catch (NumberFormatException nfe) {}
525                 return value;
526         }
527
528
529         /**
530          * Get the point pair surrounding the given photo
531          * @param inTrack track object
532          * @param inPhoto photo object
533          * @param inOffset time offset to apply to photos
534          * @return point pair resulting from correlation
535          */
536         private static PointPair getPointPairForPhoto(Track inTrack, Photo inPhoto, TimeDifference inOffset)
537         {
538                 PointPair pair = new PointPair(inPhoto);
539                 // Add/subtract offet to photo timestamp
540                 Timestamp photoStamp = inPhoto.getTimestamp().createMinusOffset(inOffset);
541                 int numPoints = inTrack.getNumPoints();
542                 for (int i=0; i<numPoints; i++)
543                 {
544                         DataPoint point = inTrack.getPoint(i);
545                         Timestamp pointStamp = point.getTimestamp();
546                         if (pointStamp != null && pointStamp.isValid())
547                         {
548                                 long numSeconds = pointStamp.getSecondsSince(photoStamp);
549                                 pair.addPoint(point, numSeconds);
550                         }
551                 }
552                 return pair;
553         }
554
555
556         /**
557          * Construct an array of the point pairs to use for correlation
558          * @return array of PointPair objects
559          */
560         private PointPair[] getPointPairs()
561         {
562                 PhotoPreviewTableModel model = (PhotoPreviewTableModel) _previewTable.getModel();
563                 int numPhotos = model.getRowCount();
564                 PointPair[] pairs = new PointPair[numPhotos];
565                 // Loop over photos in preview table model
566                 for (int i=0; i<numPhotos; i++)
567                 {
568                         PhotoPreviewTableRow row = model.getRow(i);
569                         // add all selected pairs to array (other elements remain null)
570                         if (row.getCorrelateFlag().booleanValue())
571                         {
572                                 pairs[i] = row.getPointPair();
573                         }
574                 }
575                 return pairs;
576         }
577
578         /**
579          * @return time difference of local time zone from UTC when the first photo was taken
580          */
581         private TimeDifference getTimezoneOffset()
582         {
583                 Calendar cal = null;
584                 // Base time difference on DST when first photo was taken
585                 Photo firstPhoto = _app.getTrackInfo().getPhotoList().getPhoto(0);
586                 if (firstPhoto != null && firstPhoto.getTimestamp() != null) {
587                         cal = firstPhoto.getTimestamp().getCalendar();
588                 }
589                 else {
590                         // No photo or no timestamp, just use current time
591                         cal = Calendar.getInstance();
592                 }
593                 // Both time zone offset and dst offset are based on milliseconds, so convert to seconds
594                 TimeDifference timeDiff = new TimeDifference((cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 1000);
595                 return timeDiff;
596         }
597
598
599         /**
600          * Calculate the median index to select from the table
601          * @param inModel table model
602          * @return index of entry to select from table
603          */
604         private static int getMedianIndex(PhotoSelectionTableModel inModel)
605         {
606                 // make sortable list
607                 TreeSet set = new TreeSet();
608                 // loop through rows of table adding to list
609                 int numRows = inModel.getRowCount();
610                 int i;
611                 for (i=0; i<numRows; i++)
612                 {
613                         PhotoSelectionTableRow row = inModel.getRow(i);
614                         set.add(new TimeIndexPair(row.getTimeDiff().getTotalSeconds(), i));
615                 }
616                 // pull out middle entry and return index
617                 TimeIndexPair pair = null;
618                 Iterator iterator = set.iterator();
619                 for (i=0; i<(numRows+1)/2; i++)
620                 {
621                         pair = (TimeIndexPair) iterator.next();
622                 }
623                 return pair.getIndex();
624         }
625
626
627         /**
628          * Disable the ok button
629          */
630         public void disableOkButton()
631         {
632                 if (_okButton != null) {
633                         _okButton.setEnabled(false);
634                 }
635         }
636
637
638         /**
639          * Check if the track has any uncorrelated photos
640          * @return true if there are any photos which are not connected to points
641          */
642         private boolean trackHasUncorrelatedPhotos()
643         {
644                 PhotoList photoList = _app.getTrackInfo().getPhotoList();
645                 int numPhotos = photoList.getNumPhotos();
646                 // loop over photos
647                 for (int i=0; i<numPhotos; i++)
648                 {
649                         Photo photo = photoList.getPhoto(i);
650                         if (photo != null && photo.getDataPoint() == null) {
651                                 return true;
652                         }
653                 }
654                 // no uncorrelated photos found
655                 return false;
656         }
657 }