]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/correlate/PhotoCorrelator.java
Version 4, January 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                                 final double angDistPair = DataPoint.calculateRadiansBetween(pair.getPointBefore(), pair.getPointAfter());
432                                 //System.out.println("(dist between pair is " + angDistPair + ") which means "
433                                 //      + Distance.convertRadiansToDistance(angDistPair, Distance.UNITS_METRES) + "m");
434                                 double frac = pair.getFraction();
435                                 if (frac > 0.5) {frac = 1 - frac;}
436                                 final double angDistPhoto = angDistPair * frac;
437                                 correlatePhoto = (angDistPhoto < angDistLimit);
438                         }
439                         // Don't select photos which are already correlated to the same point
440                         if (pair.getSecondsBefore() == 0L && pair.getPointBefore().getPhoto() != null
441                                 && pair.getPointBefore().getPhoto().equals(photo)) {
442                                 correlatePhoto = false;
443                         }
444                         row.setCorrelateFlag(correlatePhoto);
445                         model.addPhotoRow(row);
446                 }
447                 _previewTable.setModel(model);
448                 // Set distance units
449                 model.setDistanceUnits(getSelectedDistanceUnits());
450                 // Set column widths
451                 _previewTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
452                 final int[] colWidths = {150, 160, 100, 100, 50};
453                 for (int i=0; i<model.getColumnCount(); i++) {
454                         _previewTable.getColumnModel().getColumn(i).setPreferredWidth(colWidths[i]);
455                 }
456                 // check if any photos found
457                 _okButton.setEnabled(model.hasPhotosSelected());
458                 if (inShowWarning && !model.hasPhotosSelected())
459                 {
460                         JOptionPane.showMessageDialog(_dialog, I18nManager.getText("dialog.correlate.alloutsiderange"),
461                                 I18nManager.getText("dialog.correlate.title"), JOptionPane.ERROR_MESSAGE);
462                 }
463         }
464
465         /**
466          * Parse the time limit values entered and validate them
467          * @return TimeDifference object describing limit
468          */
469         private TimeDifference parseTimeLimit()
470         {
471                 if (!_timeLimitRadio.isSelected()) {return null;}
472                 int mins = getValue(_limitMinBox.getText());
473                 _limitMinBox.setText("" + mins);
474                 int secs = getValue(_limitSecBox.getText());
475                 _limitSecBox.setText("" + secs);
476                 if (mins <= 0 && secs <= 0) {return null;}
477                 return new TimeDifference(0, mins, secs, true);
478         }
479
480         /**
481          * Parse the distance limit value entered and validate
482          * @return angular distance in radians
483          */
484         private double parseDistanceLimit()
485         {
486                 double value = -1.0;
487                 if (_distLimitRadio.isSelected())
488                 {
489                         try
490                         {
491                                 value = Double.parseDouble(_limitDistBox.getText());
492                         }
493                         catch (NumberFormatException nfe) {}
494                 }
495                 if (value <= 0.0) {
496                         _limitDistBox.setText("0");
497                         return -1.0;
498                 }
499                 _limitDistBox.setText("" + value);
500                 return Distance.convertDistanceToRadians(value, getSelectedDistanceUnits());
501         }
502
503
504         /**
505          * @return the selected distance units from the dropdown
506          */
507         private int getSelectedDistanceUnits()
508         {
509                 final int[] distUnits = {Distance.UNITS_KILOMETRES, Distance.UNITS_METRES, Distance.UNITS_MILES};
510                 return distUnits[_distUnitsDropdown.getSelectedIndex()];
511         }
512
513
514         /**
515          * Try to parse the given string
516          * @param inText String to parse
517          * @return value if parseable, 0 otherwise
518          */
519         private static int getValue(String inText)
520         {
521                 int value = 0;
522                 try {
523                         value = Integer.parseInt(inText);
524                 }
525                 catch (NumberFormatException nfe) {}
526                 return value;
527         }
528
529
530         /**
531          * Get the point pair surrounding the given photo
532          * @param inTrack track object
533          * @param inPhoto photo object
534          * @param inOffset time offset to apply to photos
535          * @return point pair resulting from correlation
536          */
537         private static PointPair getPointPairForPhoto(Track inTrack, Photo inPhoto, TimeDifference inOffset)
538         {
539                 PointPair pair = new PointPair(inPhoto);
540                 // Add offet to photo timestamp
541                 Timestamp photoStamp = inPhoto.getTimestamp().subtractOffset(inOffset);
542                 int numPoints = inTrack.getNumPoints();
543                 for (int i=0; i<numPoints; i++)
544                 {
545                         DataPoint point = inTrack.getPoint(i);
546                         Timestamp pointStamp = point.getTimestamp();
547                         if (pointStamp != null && pointStamp.isValid())
548                         {
549                                 long numSeconds = pointStamp.getSecondsSince(photoStamp);
550                                 pair.addPoint(point, numSeconds);
551                         }
552                 }
553                 return pair;
554         }
555
556
557         /**
558          * Construct an array of the point pairs to use for correlation
559          * @return array of PointPair objects
560          */
561         private PointPair[] getPointPairs()
562         {
563                 PhotoPreviewTableModel model = (PhotoPreviewTableModel) _previewTable.getModel();
564                 int numPhotos = model.getRowCount();
565                 PointPair[] pairs = new PointPair[numPhotos];
566                 // Loop over photos in preview table model
567                 for (int i=0; i<numPhotos; i++)
568                 {
569                         PhotoPreviewTableRow row = model.getRow(i);
570                         // add all selected pairs to array (other elements remain null)
571                         if (row.getCorrelateFlag().booleanValue())
572                         {
573                                 pairs[i] = row.getPointPair();
574                         }
575                 }
576                 return pairs;
577         }
578
579         /**
580          * @return time difference of local time zone from UTC when the first photo was taken
581          */
582         private TimeDifference getTimezoneOffset()
583         {
584                 Calendar cal = null;
585                 // Base time difference on DST when first photo was taken
586                 Photo firstPhoto = _app.getTrackInfo().getPhotoList().getPhoto(0);
587                 if (firstPhoto != null && firstPhoto.getTimestamp() != null) {
588                         cal = firstPhoto.getTimestamp().getCalendar();
589                 }
590                 else {
591                         // No photo or no timestamp, just use current time
592                         cal = Calendar.getInstance();
593                 }
594                 // Both time zone offset and dst offset are based on milliseconds, so convert to seconds
595                 TimeDifference timeDiff = new TimeDifference((cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 1000);
596                 return timeDiff;
597         }
598
599
600         /**
601          * Calculate the median index to select from the table
602          * @param inModel table model
603          * @return index of entry to select from table
604          */
605         private static int getMedianIndex(PhotoSelectionTableModel inModel)
606         {
607                 // make sortable list
608                 TreeSet set = new TreeSet();
609                 // loop through rows of table adding to list
610                 int numRows = inModel.getRowCount();
611                 int i;
612                 for (i=0; i<numRows; i++)
613                 {
614                         PhotoSelectionTableRow row = inModel.getRow(i);
615                         set.add(new TimeIndexPair(row.getTimeDiff().getTotalSeconds(), i));
616                         //System.out.println("pair " + i + " has time " + row.getTimeDiff().getTotalSeconds());
617                 }
618                 // pull out middle entry and return index
619                 TimeIndexPair pair = null;
620                 Iterator iterator = set.iterator();
621                 for (i=0; i<(numRows+1)/2; i++)
622                 {
623                         pair = (TimeIndexPair) iterator.next();
624                         //System.out.println("After sorting, pair " + i + " has index " + pair.getIndex());
625                 }
626                 return pair.getIndex();
627         }
628
629
630         /**
631          * Disable the ok button
632          */
633         public void disableOkButton()
634         {
635                 if (_okButton != null)
636                 {
637                         _okButton.setEnabled(false);
638                 }
639         }
640
641
642         /**
643          * Check if the track has any uncorrelated photos
644          * @return true if there are any photos which are not connected to points
645          */
646         private boolean trackHasUncorrelatedPhotos()
647         {
648                 PhotoList photoList = _app.getTrackInfo().getPhotoList();
649                 int numPhotos = photoList.getNumPhotos();
650                 // loop over photos
651                 for (int i=0; i<numPhotos; i++)
652                 {
653                         Photo photo = photoList.getPhoto(i);
654                         if (photo != null && photo.getDataPoint() == null) {
655                                 return true;
656                         }
657                 }
658                 // no uncorrelated photos found
659                 return false;
660         }
661 }