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