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