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