]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/correlate/Correlator.java
Version 19, May 2018
[GpsPrune.git] / tim / prune / correlate / Correlator.java
1 package tim.prune.correlate;
2
3 import java.awt.BorderLayout;
4 import java.awt.Component;
5 import java.awt.Dimension;
6 import java.awt.FlowLayout;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.util.Iterator;
10 import java.util.TimeZone;
11 import java.util.TreeSet;
12
13 import javax.swing.BorderFactory;
14 import javax.swing.BoxLayout;
15 import javax.swing.ButtonGroup;
16 import javax.swing.JButton;
17 import javax.swing.JComboBox;
18 import javax.swing.JDialog;
19 import javax.swing.JLabel;
20 import javax.swing.JOptionPane;
21 import javax.swing.JPanel;
22 import javax.swing.JRadioButton;
23 import javax.swing.JScrollPane;
24 import javax.swing.JTable;
25 import javax.swing.JTextField;
26
27 import tim.prune.App;
28 import tim.prune.GenericFunction;
29 import tim.prune.I18nManager;
30 import tim.prune.config.TimezoneHelper;
31 import tim.prune.data.DataPoint;
32 import tim.prune.data.Distance;
33 import tim.prune.data.Field;
34 import tim.prune.data.MediaObject;
35 import tim.prune.data.MediaList;
36 import tim.prune.data.TimeDifference;
37 import tim.prune.data.Timestamp;
38 import tim.prune.data.Track;
39 import tim.prune.data.Unit;
40 import tim.prune.data.UnitSetLibrary;
41 import tim.prune.tips.TipManager;
42
43 /**
44  * Abstract superclass of the two correlator functions
45  */
46 public abstract class Correlator extends GenericFunction
47 {
48         protected JDialog _dialog;
49         private CardStack _cards = null;
50         private JTable _selectionTable = null;
51         protected JTable _previewTable = null;
52         private boolean _previewEnabled = false; // flag required to enable preview function on final panel
53         private boolean[] _cardEnabled = null; // flag for each card
54         private TimeZone _timezone = null;
55         private JTextField _offsetHourBox = null, _offsetMinBox = null, _offsetSecBox = null;
56         private JRadioButton _mediaLaterOption = null, _pointLaterOption = null;
57         private JRadioButton _timeLimitRadio = null, _distLimitRadio = null;
58         private JTextField _limitMinBox = null, _limitSecBox = null;
59         private JTextField _limitDistBox = null;
60         private JComboBox<String> _distUnitsDropdown = null;
61         private JButton _nextButton = null, _backButton = null;
62         protected JButton _okButton = null;
63
64
65         /**
66          * Constructor
67          * @param inApp App object to report actions to
68          */
69         public Correlator(App inApp) {
70                 super(inApp);
71         }
72
73         /**
74          * @return type key eg photo, audio
75          */
76         protected abstract String getMediaTypeKey();
77
78         /**
79          * @return media list
80          */
81         protected abstract MediaList getMediaList();
82
83         /**
84          * Begin the function by initialising and showing the dialog
85          */
86         public void begin()
87         {
88                 // Check whether track has timestamps, exit if not
89                 if (!_app.getTrackInfo().getTrack().hasData(Field.TIMESTAMP))
90                 {
91                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.correlate.notimestamps"),
92                                 I18nManager.getText(getNameKey()), JOptionPane.INFORMATION_MESSAGE);
93                         return;
94                 }
95                 // Show warning if no uncorrelated audios
96                 if (!getMediaList().hasUncorrelatedMedia())
97                 {
98                         Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")};
99                         if (JOptionPane.showOptionDialog(_parentFrame,
100                                         I18nManager.getText("dialog.correlate.nouncorrelated" + getMediaTypeKey() + "s"),
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                 // Create dialog if necessary
109                 if (_dialog == null)
110                 {
111                         _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
112                         _dialog.setLocationRelativeTo(_parentFrame);
113                         _dialog.getContentPane().add(makeDialogContents());
114                         _dialog.pack();
115                 }
116                 _okButton.setEnabled(false);
117                 // Init timezone to the currently selected one
118                 _timezone = TimezoneHelper.getSelectedTimezone();
119                 // Go to first available card
120                 int card = 0;
121                 _cardEnabled = null;
122                 while (!isCardEnabled(card)) {
123                         card++;
124                 }
125                 _cards.showCard(card);
126                 showCard(0); // does set up and next/prev enabling
127                 if (!isCardEnabled(1)) {
128                         _app.showTip(TipManager.Tip_ManuallyCorrelateOne);
129                 }
130                 _dialog.setVisible(true);
131         }
132
133         /**
134          * Make contents of correlate dialog
135          * @return JPanel containing gui elements
136          */
137         private JPanel makeDialogContents()
138         {
139                 JPanel mainPanel = new JPanel();
140                 mainPanel.setLayout(new BorderLayout());
141
142                 // Card panel in the middle
143                 _cards = new CardStack();
144
145                 // First panel (not required by photo correlator)
146                 JPanel card1 = makeFirstPanel();
147                 if (card1 == null) {card1 = new JPanel();}
148                 _cards.addCard(card1);
149
150                 // Second panel for selection of linked media
151                 _cards.addCard(makeSecondPanel());
152
153                 // Third panel for options and preview
154                 _cards.addCard(makeThirdPanel());
155                 mainPanel.add(_cards, BorderLayout.CENTER);
156
157                 // Button panel at the bottom
158                 JPanel buttonPanel = new JPanel();
159                 _backButton = new JButton(I18nManager.getText("button.back"));
160                 _backButton.addActionListener(new ActionListener() {
161                         public void actionPerformed(ActionEvent e) {
162                                 showCard(-1);
163                         }
164                 });
165                 _backButton.setEnabled(false);
166                 buttonPanel.add(_backButton);
167                 _nextButton = new JButton(I18nManager.getText("button.next"));
168                 _nextButton.addActionListener(new ActionListener() {
169                         public void actionPerformed(ActionEvent e) {
170                                 showCard(1);
171                         }
172                 });
173                 buttonPanel.add(_nextButton);
174                 _okButton = new JButton(I18nManager.getText("button.ok"));
175                 _okButton.addActionListener(new ActionListener()
176                         {
177                                 public void actionPerformed(ActionEvent e)
178                                 {
179                                         finishCorrelation();
180                                         _dialog.dispose();
181                                 }
182                         });
183                 _okButton.setEnabled(false);
184                 buttonPanel.add(_okButton);
185                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
186                 cancelButton.addActionListener(new ActionListener() {
187                         public void actionPerformed(ActionEvent e) {
188                                 _dialog.dispose();
189                         }
190                 });
191                 buttonPanel.add(cancelButton);
192                 mainPanel.add(buttonPanel, BorderLayout.SOUTH);
193                 return mainPanel;
194         }
195
196         /**
197          * Construct a table model for the photo / audio selection table
198          * @return table model
199          */
200         protected MediaSelectionTableModel makeSelectionTableModel()
201         {
202                 MediaList mediaList = getMediaList();
203                 MediaSelectionTableModel model = new MediaSelectionTableModel(
204                         "dialog.correlate.select." + getMediaTypeKey() + "name",
205                         "dialog.correlate.select." + getMediaTypeKey() + "later");
206                 int numMedia = mediaList.getNumMedia();
207                 for (int i=0; i<numMedia; i++)
208                 {
209                         MediaObject media = mediaList.getMedia(i);
210                         // For working out time differences, can't use media which already had point information
211                         if (media.getDataPoint() != null && media.getDataPoint().hasTimestamp()
212                                 && media.getOriginalStatus() == MediaObject.Status.NOT_CONNECTED)
213                         {
214                                 // Calculate time difference, add to table model
215                                 long timeDiff = getMediaTimestamp(media).getSecondsSince(media.getDataPoint().getTimestamp(), _timezone);
216                                 model.addMedia(media, timeDiff);
217                         }
218                 }
219                 return model;
220         }
221
222         /**
223          * Group the two radio buttons together with a ButtonGroup
224          * @param inButton1 first radio button
225          * @param inButton2 second radio button
226          */
227         protected static void groupRadioButtons(JRadioButton inButton1, JRadioButton inButton2)
228         {
229                 ButtonGroup buttonGroup = new ButtonGroup();
230                 buttonGroup.add(inButton1);
231                 buttonGroup.add(inButton2);
232                 inButton1.setSelected(true);
233         }
234
235
236         /**
237          * Try to parse the given string
238          * @param inText String to parse
239          * @return value if parseable, 0 otherwise
240          */
241         protected static int getValue(String inText)
242         {
243                 int value = 0;
244                 try {
245                         value = Integer.parseInt(inText);
246                 }
247                 catch (NumberFormatException nfe) {}
248                 return value;
249         }
250
251
252         /**
253          * Calculate the median index to select from the table
254          * @param inModel table model
255          * @return index of entry to select from table
256          */
257         protected static int getMedianIndex(MediaSelectionTableModel inModel)
258         {
259                 // make sortable list
260                 TreeSet<TimeIndexPair> set = new TreeSet<TimeIndexPair>();
261                 // loop through rows of table adding to list
262                 int numRows = inModel.getRowCount();
263                 int i;
264                 for (i=0; i<numRows; i++)
265                 {
266                         MediaSelectionTableRow row = inModel.getRow(i);
267                         set.add(new TimeIndexPair(row.getTimeDiff().getTotalSeconds(), i));
268                 }
269                 // pull out middle entry and return index
270                 TimeIndexPair pair = null;
271                 Iterator<TimeIndexPair> iterator = set.iterator();
272                 for (i=0; i<(numRows+1)/2; i++)
273                 {
274                         pair = iterator.next();
275                 }
276                 return pair.getIndex();
277         }
278
279
280         /**
281          * Disable the ok button
282          */
283         public void disableOkButton()
284         {
285                 if (_okButton != null) {
286                         _okButton.setEnabled(false);
287                 }
288         }
289
290         /**
291          * @return gui components for first panel, or null if empty
292          */
293         protected JPanel makeFirstPanel() {
294                 return null;
295         }
296
297         /**
298          * Make the second panel for the selection screen
299          * @return JPanel object containing gui elements
300          */
301         private JPanel makeSecondPanel()
302         {
303                 JPanel card = new JPanel();
304                 card.setLayout(new BorderLayout(10, 10));
305                 JLabel introLabel = new JLabel(I18nManager.getText(
306                         "dialog.correlate." + getMediaTypeKey() + "select.intro"));
307                 introLabel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
308                 card.add(introLabel, BorderLayout.NORTH);
309                 // table doesn't have model yet - that will be attached later
310                 _selectionTable = new JTable();
311                 JScrollPane photoScrollPane = new JScrollPane(_selectionTable);
312                 photoScrollPane.setPreferredSize(new Dimension(400, 100));
313                 card.add(photoScrollPane, BorderLayout.CENTER);
314                 return card;
315         }
316
317
318         /**
319          * Make contents of third panel including options and preview
320          * @return JPanel containing gui elements
321          */
322         private JPanel makeThirdPanel()
323         {
324                 OptionsChangedListener optionsChangedListener = new OptionsChangedListener(this);
325                 // Second panel for options
326                 JPanel card2 = new JPanel();
327                 card2.setLayout(new BorderLayout());
328                 JPanel card2Top = new JPanel();
329                 card2Top.setLayout(new BoxLayout(card2Top, BoxLayout.Y_AXIS));
330                 JLabel introLabel = new JLabel(I18nManager.getText("dialog.correlate.options.intro"));
331                 introLabel.setBorder(BorderFactory.createEmptyBorder(8, 6, 5, 6));
332                 card2Top.add(introLabel);
333                 // time offset section
334                 JPanel offsetPanel = new JPanel();
335                 offsetPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.offsetpanel")));
336                 offsetPanel.setLayout(new BoxLayout(offsetPanel, BoxLayout.Y_AXIS));
337                 JPanel offsetPanelTop = new JPanel();
338                 offsetPanelTop.setLayout(new FlowLayout());
339                 offsetPanelTop.setBorder(null);
340                 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset") + ": "));
341                 _offsetHourBox = new JTextField(3);
342                 _offsetHourBox.addKeyListener(optionsChangedListener);
343                 offsetPanelTop.add(_offsetHourBox);
344                 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.hours")));
345                 _offsetMinBox = new JTextField(3);
346                 _offsetMinBox.addKeyListener(optionsChangedListener);
347                 offsetPanelTop.add(_offsetMinBox);
348                 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
349                 _offsetSecBox = new JTextField(3);
350                 _offsetSecBox.addKeyListener(optionsChangedListener);
351                 offsetPanelTop.add(_offsetSecBox);
352                 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
353                 offsetPanel.add(offsetPanelTop);
354
355                 // radio buttons for photo / point later
356                 JPanel offsetPanelBot = new JPanel();
357                 offsetPanelBot.setLayout(new FlowLayout());
358                 offsetPanelBot.setBorder(null);
359                 _mediaLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options." + getMediaTypeKey() + "later"));
360                 _pointLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.pointlater" + getMediaTypeKey()));
361                 _mediaLaterOption.addItemListener(optionsChangedListener);
362                 _pointLaterOption.addItemListener(optionsChangedListener);
363                 ButtonGroup laterGroup = new ButtonGroup();
364                 laterGroup.add(_mediaLaterOption);
365                 laterGroup.add(_pointLaterOption);
366                 offsetPanelBot.add(_mediaLaterOption);
367                 offsetPanelBot.add(_pointLaterOption);
368                 offsetPanel.add(offsetPanelBot);
369                 offsetPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
370                 card2Top.add(offsetPanel);
371
372                 // listener for radio buttons
373                 ActionListener radioListener = new ActionListener() {
374                         public void actionPerformed(ActionEvent e) {
375                                 enableEditBoxes();
376                         }
377                 };
378                 // time limits section
379                 JPanel limitsPanel = new JPanel();
380                 limitsPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.limitspanel")));
381                 limitsPanel.setLayout(new BoxLayout(limitsPanel, BoxLayout.Y_AXIS));
382                 JPanel timeLimitPanel = new JPanel();
383                 timeLimitPanel.setLayout(new FlowLayout());
384                 JRadioButton noTimeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.notimelimit"));
385                 noTimeLimitRadio.addItemListener(optionsChangedListener);
386                 noTimeLimitRadio.addActionListener(radioListener);
387                 timeLimitPanel.add(noTimeLimitRadio);
388                 _timeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.timelimit") + ": ");
389                 _timeLimitRadio.addItemListener(optionsChangedListener);
390                 _timeLimitRadio.addActionListener(radioListener);
391                 timeLimitPanel.add(_timeLimitRadio);
392                 groupRadioButtons(noTimeLimitRadio, _timeLimitRadio);
393                 _limitMinBox = new JTextField(3);
394                 _limitMinBox.addKeyListener(optionsChangedListener);
395                 timeLimitPanel.add(_limitMinBox);
396                 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
397                 _limitSecBox = new JTextField(3);
398                 _limitSecBox.addKeyListener(optionsChangedListener);
399                 timeLimitPanel.add(_limitSecBox);
400                 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
401                 limitsPanel.add(timeLimitPanel);
402                 // distance limits
403                 JPanel distLimitPanel = new JPanel();
404                 distLimitPanel.setLayout(new FlowLayout());
405                 JRadioButton noDistLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.nodistancelimit"));
406                 noDistLimitRadio.addItemListener(optionsChangedListener);
407                 noDistLimitRadio.addActionListener(radioListener);
408                 distLimitPanel.add(noDistLimitRadio);
409                 _distLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.distancelimit") + ": ");
410                 _distLimitRadio.addItemListener(optionsChangedListener);
411                 _distLimitRadio.addActionListener(radioListener);
412                 distLimitPanel.add(_distLimitRadio);
413                 groupRadioButtons(noDistLimitRadio, _distLimitRadio);
414                 _limitDistBox = new JTextField(4);
415                 _limitDistBox.addKeyListener(optionsChangedListener);
416                 distLimitPanel.add(_limitDistBox);
417                 String[] distUnitsOptions = {I18nManager.getText("units.kilometres"), I18nManager.getText("units.metres"),
418                         I18nManager.getText("units.miles")};
419                 _distUnitsDropdown = new JComboBox<String>(distUnitsOptions);
420                 _distUnitsDropdown.addItemListener(optionsChangedListener);
421                 distLimitPanel.add(_distUnitsDropdown);
422                 limitsPanel.add(distLimitPanel);
423                 limitsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
424                 card2Top.add(limitsPanel);
425
426                 // preview button
427                 JButton previewButton = new JButton(I18nManager.getText("button.preview"));
428                 previewButton.addActionListener(new ActionListener() {
429                         public void actionPerformed(ActionEvent e) {
430                                 createPreview(true);
431                         }
432                 });
433                 card2Top.add(previewButton);
434                 card2.add(card2Top, BorderLayout.NORTH);
435                 // preview
436                 _previewTable = new JTable(new MediaPreviewTableModel("dialog.correlate.select." + getMediaTypeKey() + "name"));
437                 JScrollPane previewScrollPane = new JScrollPane(_previewTable);
438                 previewScrollPane.setPreferredSize(new Dimension(300, 100));
439                 card2.add(previewScrollPane, BorderLayout.CENTER);
440                 return card2;
441         }
442
443
444         /**
445          * Go to the next or previous card in the stack
446          * @param increment 1 for next, -1 for previous card
447          */
448         private void showCard(int increment)
449         {
450                 int currCard = _cards.getCurrentCardIndex();
451                 int next = currCard + increment;
452                 if (!isCardEnabled(next)) {
453                         next += increment;
454                 }
455                 setupCard(next);
456                 _backButton.setEnabled(next > 0 && (isCardEnabled(next-1) || isCardEnabled(next-2)));
457                 _nextButton.setEnabled(next < (_cards.getNumCards()-1));
458                 _cards.showCard(next);
459         }
460
461         /**
462          * @param inCardNum index of card
463          * @return true if specified card is enabled
464          */
465         private boolean isCardEnabled(int inCardNum)
466         {
467                 if (_cardEnabled == null) {
468                         _cardEnabled = getCardEnabledFlags();
469                 }
470                 return (inCardNum >= 0 && inCardNum < _cardEnabled.length && _cardEnabled[inCardNum]);
471         }
472
473         /**
474          * @return array of boolean flags denoting availability of cards
475          */
476         protected boolean[] getCardEnabledFlags()
477         {
478                 // by default first is off and third is always on; second depends on selection table
479                 return new boolean[] {false, makeSelectionTableModel().getRowCount() > 0, true};
480         }
481
482         /**
483          * Set up the specified card
484          * @param inCardNum index of card
485          */
486         protected void setupCard(int inCardNum)
487         {
488                 _previewEnabled = false;
489                 if (inCardNum == 1)
490                 {
491                         // set up photo selection card
492                         MediaSelectionTableModel model = makeSelectionTableModel();
493                         _selectionTable.setModel(model);
494                         for (int i=0; i<model.getColumnCount(); i++) {
495                                 _selectionTable.getColumnModel().getColumn(i).setPreferredWidth(i==3?50:150);
496                         }
497                         // Calculate median time difference, select corresponding row of table
498                         int preselectedIndex = model.getRowCount() < 3 ? 0 : getMedianIndex(model);
499                         _selectionTable.getSelectionModel().setSelectionInterval(preselectedIndex, preselectedIndex);
500                         _nextButton.requestFocus();
501                 }
502                 else if (inCardNum == 2)
503                 {
504                         // set up the options/preview card - first check for given time difference
505                         TimeDifference timeDiff = null;
506                         if (isCardEnabled(1))
507                         {
508                                 int rowNum = _selectionTable.getSelectedRow();
509                                 if (rowNum < 0) {rowNum = 0;}
510                                 MediaSelectionTableRow selectedRow =
511                                         ((MediaSelectionTableModel) _selectionTable.getModel()).getRow(rowNum);
512                                 timeDiff = selectedRow.getTimeDiff();
513                         }
514                         setupPreviewCard(timeDiff, getMediaList().getMedia(0));
515                 }
516                 // enable ok button if any photos have been selected
517                 _okButton.setEnabled(inCardNum == 2 && ((MediaPreviewTableModel) _previewTable.getModel()).hasAnySelected());
518         }
519
520         /**
521          * Enable or disable the edit boxes according to the radio button selections
522          */
523         private void enableEditBoxes()
524         {
525                 // enable/disable text field for distance input
526                 _limitDistBox.setEnabled(_distLimitRadio.isSelected());
527                 // and for time limits
528                 _limitMinBox.setEnabled(_timeLimitRadio.isSelected());
529                 _limitSecBox.setEnabled(_timeLimitRadio.isSelected());
530         }
531
532         /**
533          * Parse the time limit values entered and validate them
534          * @return TimeDifference object describing limit
535          */
536         protected TimeDifference parseTimeLimit()
537         {
538                 if (!_timeLimitRadio.isSelected()) {return null;}
539                 int mins = getValue(_limitMinBox.getText());
540                 _limitMinBox.setText("" + mins);
541                 int secs = getValue(_limitSecBox.getText());
542                 _limitSecBox.setText("" + secs);
543                 if (mins <= 0 && secs <= 0) {return null;}
544                 return new TimeDifference(0, mins, secs, true);
545         }
546
547         /**
548          * Parse the distance limit value entered and validate
549          * @return angular distance in radians
550          */
551         protected double parseDistanceLimit()
552         {
553                 double value = -1.0;
554                 if (_distLimitRadio.isSelected())
555                 {
556                         try {
557                                 value = Double.parseDouble(_limitDistBox.getText());
558                         }
559                         catch (NumberFormatException nfe) {}
560                 }
561                 if (value <= 0.0) {
562                         _limitDistBox.setText("0");
563                         return -1.0;
564                 }
565                 _limitDistBox.setText("" + value);
566                 return Distance.convertDistanceToRadians(value, getSelectedDistanceUnits());
567         }
568
569
570         /**
571          * @return the selected distance units from the dropdown
572          */
573         protected Unit getSelectedDistanceUnits()
574         {
575                 final Unit[] distUnits = {UnitSetLibrary.UNITS_KILOMETRES, UnitSetLibrary.UNITS_METRES, UnitSetLibrary.UNITS_MILES};
576                 return distUnits[_distUnitsDropdown.getSelectedIndex()];
577         }
578
579         /**
580          * Create a preview of the correlate action using the selected time difference
581          * @param inFromButton true if triggered from button press, false if automatic
582          */
583         public void createPreview(boolean inFromButton)
584         {
585                 // Exit if still on first panel
586                 if (!_previewEnabled) {
587                         return;
588                 }
589                 // Create a TimeDifference based on the edit boxes
590                 int numHours = getValue(_offsetHourBox.getText());
591                 int numMins = getValue(_offsetMinBox.getText());
592                 int numSecs = getValue(_offsetSecBox.getText());
593                 boolean isPos = _mediaLaterOption.isSelected();
594                 createPreview(new TimeDifference(numHours, numMins, numSecs, isPos), inFromButton);
595         }
596
597         /**
598          * Set up the final card using the given time difference and show it
599          * @param inTimeDiff time difference to use for time offsets
600          * @param inFirstMedia first media object to use for calculating timezone
601          */
602         protected void setupPreviewCard(TimeDifference inTimeDiff, MediaObject inFirstMedia)
603         {
604                 _previewEnabled = false;
605                 TimeDifference timeDiff = inTimeDiff;
606                 if (timeDiff == null)
607                 {
608                         // No time difference available, so try with zero
609                         timeDiff = new TimeDifference(0L);
610                 }
611                 // Use time difference to set edit boxes
612                 _offsetHourBox.setText("" + timeDiff.getNumHours());
613                 _offsetMinBox.setText("" + timeDiff.getNumMinutes());
614                 _offsetSecBox.setText("" + timeDiff.getNumSeconds());
615                 _mediaLaterOption.setSelected(timeDiff.getIsPositive());
616                 _pointLaterOption.setSelected(!timeDiff.getIsPositive());
617                 _previewEnabled = true;
618                 enableEditBoxes();
619                 createPreview(timeDiff, true);
620         }
621
622         /**
623          * Create a preview of the correlate action using the selected time difference
624          * @param inTimeDiff TimeDifference to use for preview
625          * @param inShowWarning true to show warning if all points out of range
626          */
627         protected abstract void createPreview(TimeDifference inTimeDiff, boolean inShowWarning);
628
629
630         /**
631          * Get the timestamp of the given media
632          * @param inMedia media object
633          * @return normally just returns the media timestamp, overridden by audio correlator
634          */
635         protected Timestamp getMediaTimestamp(MediaObject inMedia)
636         {
637                 return inMedia.getTimestamp();
638         }
639
640         /**
641          * Get the point pair surrounding the given media item
642          * @param inTrack track object
643          * @param inMedia media object
644          * @param inOffset time offset to apply
645          * @return point pair resulting from correlation
646          */
647         protected PointMediaPair getPointPairForMedia(Track inTrack, MediaObject inMedia, TimeDifference inOffset)
648         {
649                 PointMediaPair pair = new PointMediaPair(inMedia);
650                 if (inMedia.hasTimestamp())
651                 {
652                         // Add/subtract offset to media timestamp
653                         Timestamp mediaStamp = getMediaTimestamp(inMedia);
654                         int numPoints = inTrack.getNumPoints();
655                         for (int i=0; i<numPoints; i++)
656                         {
657                                 DataPoint point = inTrack.getPoint(i);
658                                 if (point.getPhoto() == null && point.getAudio() == null)
659                                 {
660                                         Timestamp pointStamp = point.getTimestamp();
661                                         if (pointStamp != null && pointStamp.isValid())
662                                         {
663                                                 long numSeconds = pointStamp.getSecondsSince(mediaStamp, _timezone)
664                                                         + inOffset.getTotalSeconds();
665                                                 pair.addPoint(point, numSeconds);
666                                         }
667                                 }
668                         }
669                 }
670                 return pair;
671         }
672
673
674         /**
675          * Finish the correlation
676          */
677         protected abstract void finishCorrelation();
678
679         /**
680          * Construct an array of the point pairs to use for correlation
681          * @return array of PointMediaPair objects
682          */
683         protected PointMediaPair[] getPointPairs()
684         {
685                 MediaPreviewTableModel model = (MediaPreviewTableModel) _previewTable.getModel();
686                 int numMedia = model.getRowCount();
687                 PointMediaPair[] pairs = new PointMediaPair[numMedia];
688                 // Loop over items in preview table model
689                 for (int i=0; i<numMedia; i++)
690                 {
691                         MediaPreviewTableRow row = model.getRow(i);
692                         // add all selected pairs to array (other elements remain null)
693                         if (row.getCorrelateFlag().booleanValue()) {
694                                 pairs[i] = row.getPointPair();
695                         }
696                 }
697                 return pairs;
698         }
699 }