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