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