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