1 package tim.prune.correlate;
3 import java.awt.BorderLayout;
4 import java.awt.Component;
5 import java.awt.Dimension;
6 import java.awt.FlowLayout;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.util.Iterator;
10 import java.util.TimeZone;
11 import java.util.TreeSet;
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;
28 import tim.prune.GenericFunction;
29 import tim.prune.I18nManager;
30 import tim.prune.config.TimezoneHelper;
31 import tim.prune.data.DataPoint;
32 import tim.prune.data.Distance;
33 import tim.prune.data.Field;
34 import tim.prune.data.MediaObject;
35 import tim.prune.data.MediaList;
36 import tim.prune.data.TimeDifference;
37 import tim.prune.data.Timestamp;
38 import tim.prune.data.Track;
39 import tim.prune.data.Unit;
40 import tim.prune.data.UnitSetLibrary;
41 import tim.prune.tips.TipManager;
44 * Abstract superclass of the two correlator functions
46 public abstract class Correlator extends GenericFunction
48 protected JDialog _dialog;
49 private CardStack _cards = null;
50 private JTable _selectionTable = null;
51 protected JTable _previewTable = null;
52 private boolean _previewEnabled = false; // flag required to enable preview function on final panel
53 private boolean[] _cardEnabled = null; // flag for each card
54 private TimeZone _timezone = null;
55 private JTextField _offsetHourBox = null, _offsetMinBox = null, _offsetSecBox = null;
56 private JRadioButton _mediaLaterOption = null, _pointLaterOption = null;
57 private JRadioButton _timeLimitRadio = null, _distLimitRadio = null;
58 private JTextField _limitMinBox = null, _limitSecBox = null;
59 private JTextField _limitDistBox = null;
60 private JComboBox<String> _distUnitsDropdown = null;
61 private JButton _nextButton = null, _backButton = null;
62 protected JButton _okButton = null;
67 * @param inApp App object to report actions to
69 public Correlator(App inApp) {
74 * @return type key eg photo, audio
76 protected abstract String getMediaTypeKey();
81 protected abstract MediaList getMediaList();
84 * Begin the function by initialising and showing the dialog
88 // Check whether track has timestamps, exit if not
89 if (!_app.getTrackInfo().getTrack().hasData(Field.TIMESTAMP))
91 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.correlate.notimestamps"),
92 I18nManager.getText(getNameKey()), JOptionPane.INFORMATION_MESSAGE);
95 // Show warning if no uncorrelated audios
96 if (!getMediaList().hasUncorrelatedMedia())
98 Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")};
99 if (JOptionPane.showOptionDialog(_parentFrame,
100 I18nManager.getText("dialog.correlate.nouncorrelated" + getMediaTypeKey() + "s"),
101 I18nManager.getText(getNameKey()), JOptionPane.YES_NO_OPTION,
102 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
103 == JOptionPane.NO_OPTION)
108 // Create dialog if necessary
111 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
112 _dialog.setLocationRelativeTo(_parentFrame);
113 _dialog.getContentPane().add(makeDialogContents());
116 _okButton.setEnabled(false);
117 // Init timezone to the currently selected one
118 _timezone = TimezoneHelper.getSelectedTimezone();
119 // Go to first available card
122 while (!isCardEnabled(card)) {
125 _cards.showCard(card);
126 showCard(0); // does set up and next/prev enabling
127 if (!isCardEnabled(1)) {
128 _app.showTip(TipManager.Tip_ManuallyCorrelateOne);
130 _dialog.setVisible(true);
134 * Make contents of correlate dialog
135 * @return JPanel containing gui elements
137 private JPanel makeDialogContents()
139 JPanel mainPanel = new JPanel();
140 mainPanel.setLayout(new BorderLayout());
142 // Card panel in the middle
143 _cards = new CardStack();
145 // First panel (not required by photo correlator)
146 JPanel card1 = makeFirstPanel();
147 if (card1 == null) {card1 = new JPanel();}
148 _cards.addCard(card1);
150 // Second panel for selection of linked media
151 _cards.addCard(makeSecondPanel());
153 // Third panel for options and preview
154 _cards.addCard(makeThirdPanel());
155 mainPanel.add(_cards, BorderLayout.CENTER);
157 // Button panel at the bottom
158 JPanel buttonPanel = new JPanel();
159 _backButton = new JButton(I18nManager.getText("button.back"));
160 _backButton.addActionListener(new ActionListener() {
161 public void actionPerformed(ActionEvent e) {
165 _backButton.setEnabled(false);
166 buttonPanel.add(_backButton);
167 _nextButton = new JButton(I18nManager.getText("button.next"));
168 _nextButton.addActionListener(new ActionListener() {
169 public void actionPerformed(ActionEvent e) {
173 buttonPanel.add(_nextButton);
174 _okButton = new JButton(I18nManager.getText("button.ok"));
175 _okButton.addActionListener(new ActionListener()
177 public void actionPerformed(ActionEvent e)
183 _okButton.setEnabled(false);
184 buttonPanel.add(_okButton);
185 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
186 cancelButton.addActionListener(new ActionListener() {
187 public void actionPerformed(ActionEvent e) {
191 buttonPanel.add(cancelButton);
192 mainPanel.add(buttonPanel, BorderLayout.SOUTH);
197 * Construct a table model for the photo / audio selection table
198 * @return table model
200 protected MediaSelectionTableModel makeSelectionTableModel()
202 MediaList mediaList = getMediaList();
203 MediaSelectionTableModel model = new MediaSelectionTableModel(
204 "dialog.correlate.select." + getMediaTypeKey() + "name",
205 "dialog.correlate.select." + getMediaTypeKey() + "later");
206 int numMedia = mediaList.getNumMedia();
207 for (int i=0; i<numMedia; i++)
209 MediaObject media = mediaList.getMedia(i);
210 // For working out time differences, can't use media which already had point information
211 if (media.getDataPoint() != null && media.getDataPoint().hasTimestamp()
212 && media.getOriginalStatus() == MediaObject.Status.NOT_CONNECTED)
214 // Calculate time difference, add to table model
215 long timeDiff = getMediaTimestamp(media).getSecondsSince(media.getDataPoint().getTimestamp(), _timezone);
216 model.addMedia(media, timeDiff);
223 * Group the two radio buttons together with a ButtonGroup
224 * @param inButton1 first radio button
225 * @param inButton2 second radio button
227 protected static void groupRadioButtons(JRadioButton inButton1, JRadioButton inButton2)
229 ButtonGroup buttonGroup = new ButtonGroup();
230 buttonGroup.add(inButton1);
231 buttonGroup.add(inButton2);
232 inButton1.setSelected(true);
237 * Try to parse the given string
238 * @param inText String to parse
239 * @return value if parseable, 0 otherwise
241 protected static int getValue(String inText)
245 value = Integer.parseInt(inText);
247 catch (NumberFormatException nfe) {}
253 * Calculate the median index to select from the table
254 * @param inModel table model
255 * @return index of entry to select from table
257 protected static int getMedianIndex(MediaSelectionTableModel inModel)
259 // make sortable list
260 TreeSet<TimeIndexPair> set = new TreeSet<TimeIndexPair>();
261 // loop through rows of table adding to list
262 int numRows = inModel.getRowCount();
264 for (i=0; i<numRows; i++)
266 MediaSelectionTableRow row = inModel.getRow(i);
267 set.add(new TimeIndexPair(row.getTimeDiff().getTotalSeconds(), i));
269 // pull out middle entry and return index
270 TimeIndexPair pair = null;
271 Iterator<TimeIndexPair> iterator = set.iterator();
272 for (i=0; i<(numRows+1)/2; i++)
274 pair = iterator.next();
276 return pair.getIndex();
281 * Disable the ok button
283 public void disableOkButton()
285 if (_okButton != null) {
286 _okButton.setEnabled(false);
291 * @return gui components for first panel, or null if empty
293 protected JPanel makeFirstPanel() {
298 * Make the second panel for the selection screen
299 * @return JPanel object containing gui elements
301 private JPanel makeSecondPanel()
303 JPanel card = new JPanel();
304 card.setLayout(new BorderLayout(10, 10));
305 JLabel introLabel = new JLabel(I18nManager.getText(
306 "dialog.correlate." + getMediaTypeKey() + "select.intro"));
307 introLabel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
308 card.add(introLabel, BorderLayout.NORTH);
309 // table doesn't have model yet - that will be attached later
310 _selectionTable = new JTable();
311 JScrollPane photoScrollPane = new JScrollPane(_selectionTable);
312 photoScrollPane.setPreferredSize(new Dimension(400, 100));
313 card.add(photoScrollPane, BorderLayout.CENTER);
319 * Make contents of third panel including options and preview
320 * @return JPanel containing gui elements
322 private JPanel makeThirdPanel()
324 OptionsChangedListener optionsChangedListener = new OptionsChangedListener(this);
325 // Second panel for options
326 JPanel card2 = new JPanel();
327 card2.setLayout(new BorderLayout());
328 JPanel card2Top = new JPanel();
329 card2Top.setLayout(new BoxLayout(card2Top, BoxLayout.Y_AXIS));
330 JLabel introLabel = new JLabel(I18nManager.getText("dialog.correlate.options.intro"));
331 introLabel.setBorder(BorderFactory.createEmptyBorder(8, 6, 5, 6));
332 card2Top.add(introLabel);
333 // time offset section
334 JPanel offsetPanel = new JPanel();
335 offsetPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.offsetpanel")));
336 offsetPanel.setLayout(new BoxLayout(offsetPanel, BoxLayout.Y_AXIS));
337 JPanel offsetPanelTop = new JPanel();
338 offsetPanelTop.setLayout(new FlowLayout());
339 offsetPanelTop.setBorder(null);
340 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset") + ": "));
341 _offsetHourBox = new JTextField(3);
342 _offsetHourBox.addKeyListener(optionsChangedListener);
343 offsetPanelTop.add(_offsetHourBox);
344 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.hours")));
345 _offsetMinBox = new JTextField(3);
346 _offsetMinBox.addKeyListener(optionsChangedListener);
347 offsetPanelTop.add(_offsetMinBox);
348 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
349 _offsetSecBox = new JTextField(3);
350 _offsetSecBox.addKeyListener(optionsChangedListener);
351 offsetPanelTop.add(_offsetSecBox);
352 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
353 offsetPanel.add(offsetPanelTop);
355 // radio buttons for photo / point later
356 JPanel offsetPanelBot = new JPanel();
357 offsetPanelBot.setLayout(new FlowLayout());
358 offsetPanelBot.setBorder(null);
359 _mediaLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options." + getMediaTypeKey() + "later"));
360 _pointLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.pointlater" + getMediaTypeKey()));
361 _mediaLaterOption.addItemListener(optionsChangedListener);
362 _pointLaterOption.addItemListener(optionsChangedListener);
363 ButtonGroup laterGroup = new ButtonGroup();
364 laterGroup.add(_mediaLaterOption);
365 laterGroup.add(_pointLaterOption);
366 offsetPanelBot.add(_mediaLaterOption);
367 offsetPanelBot.add(_pointLaterOption);
368 offsetPanel.add(offsetPanelBot);
369 offsetPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
370 card2Top.add(offsetPanel);
372 // listener for radio buttons
373 ActionListener radioListener = new ActionListener() {
374 public void actionPerformed(ActionEvent e) {
378 // time limits section
379 JPanel limitsPanel = new JPanel();
380 limitsPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.limitspanel")));
381 limitsPanel.setLayout(new BoxLayout(limitsPanel, BoxLayout.Y_AXIS));
382 JPanel timeLimitPanel = new JPanel();
383 timeLimitPanel.setLayout(new FlowLayout());
384 JRadioButton noTimeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.notimelimit"));
385 noTimeLimitRadio.addItemListener(optionsChangedListener);
386 noTimeLimitRadio.addActionListener(radioListener);
387 timeLimitPanel.add(noTimeLimitRadio);
388 _timeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.timelimit") + ": ");
389 _timeLimitRadio.addItemListener(optionsChangedListener);
390 _timeLimitRadio.addActionListener(radioListener);
391 timeLimitPanel.add(_timeLimitRadio);
392 groupRadioButtons(noTimeLimitRadio, _timeLimitRadio);
393 _limitMinBox = new JTextField(3);
394 _limitMinBox.addKeyListener(optionsChangedListener);
395 timeLimitPanel.add(_limitMinBox);
396 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
397 _limitSecBox = new JTextField(3);
398 _limitSecBox.addKeyListener(optionsChangedListener);
399 timeLimitPanel.add(_limitSecBox);
400 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
401 limitsPanel.add(timeLimitPanel);
403 JPanel distLimitPanel = new JPanel();
404 distLimitPanel.setLayout(new FlowLayout());
405 JRadioButton noDistLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.nodistancelimit"));
406 noDistLimitRadio.addItemListener(optionsChangedListener);
407 noDistLimitRadio.addActionListener(radioListener);
408 distLimitPanel.add(noDistLimitRadio);
409 _distLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.distancelimit") + ": ");
410 _distLimitRadio.addItemListener(optionsChangedListener);
411 _distLimitRadio.addActionListener(radioListener);
412 distLimitPanel.add(_distLimitRadio);
413 groupRadioButtons(noDistLimitRadio, _distLimitRadio);
414 _limitDistBox = new JTextField(4);
415 _limitDistBox.addKeyListener(optionsChangedListener);
416 distLimitPanel.add(_limitDistBox);
417 String[] distUnitsOptions = {I18nManager.getText("units.kilometres"), I18nManager.getText("units.metres"),
418 I18nManager.getText("units.miles")};
419 _distUnitsDropdown = new JComboBox<String>(distUnitsOptions);
420 _distUnitsDropdown.addItemListener(optionsChangedListener);
421 distLimitPanel.add(_distUnitsDropdown);
422 limitsPanel.add(distLimitPanel);
423 limitsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
424 card2Top.add(limitsPanel);
427 JButton previewButton = new JButton(I18nManager.getText("button.preview"));
428 previewButton.addActionListener(new ActionListener() {
429 public void actionPerformed(ActionEvent e) {
433 card2Top.add(previewButton);
434 card2.add(card2Top, BorderLayout.NORTH);
436 _previewTable = new JTable(new MediaPreviewTableModel("dialog.correlate.select." + getMediaTypeKey() + "name"));
437 JScrollPane previewScrollPane = new JScrollPane(_previewTable);
438 previewScrollPane.setPreferredSize(new Dimension(300, 100));
439 card2.add(previewScrollPane, BorderLayout.CENTER);
445 * Go to the next or previous card in the stack
446 * @param increment 1 for next, -1 for previous card
448 private void showCard(int increment)
450 int currCard = _cards.getCurrentCardIndex();
451 int next = currCard + increment;
452 if (!isCardEnabled(next)) {
456 _backButton.setEnabled(next > 0 && (isCardEnabled(next-1) || isCardEnabled(next-2)));
457 _nextButton.setEnabled(next < (_cards.getNumCards()-1));
458 _cards.showCard(next);
462 * @param inCardNum index of card
463 * @return true if specified card is enabled
465 private boolean isCardEnabled(int inCardNum)
467 if (_cardEnabled == null) {
468 _cardEnabled = getCardEnabledFlags();
470 return (inCardNum >= 0 && inCardNum < _cardEnabled.length && _cardEnabled[inCardNum]);
474 * @return array of boolean flags denoting availability of cards
476 protected boolean[] getCardEnabledFlags()
478 // by default first is off and third is always on; second depends on selection table
479 return new boolean[] {false, makeSelectionTableModel().getRowCount() > 0, true};
483 * Set up the specified card
484 * @param inCardNum index of card
486 protected void setupCard(int inCardNum)
488 _previewEnabled = false;
491 // set up photo selection card
492 MediaSelectionTableModel model = makeSelectionTableModel();
493 _selectionTable.setModel(model);
494 for (int i=0; i<model.getColumnCount(); i++) {
495 _selectionTable.getColumnModel().getColumn(i).setPreferredWidth(i==3?50:150);
497 // Calculate median time difference, select corresponding row of table
498 int preselectedIndex = model.getRowCount() < 3 ? 0 : getMedianIndex(model);
499 _selectionTable.getSelectionModel().setSelectionInterval(preselectedIndex, preselectedIndex);
500 _nextButton.requestFocus();
502 else if (inCardNum == 2)
504 // set up the options/preview card - first check for given time difference
505 TimeDifference timeDiff = null;
506 if (isCardEnabled(1))
508 int rowNum = _selectionTable.getSelectedRow();
509 if (rowNum < 0) {rowNum = 0;}
510 MediaSelectionTableRow selectedRow =
511 ((MediaSelectionTableModel) _selectionTable.getModel()).getRow(rowNum);
512 timeDiff = selectedRow.getTimeDiff();
514 setupPreviewCard(timeDiff, getMediaList().getMedia(0));
516 // enable ok button if any photos have been selected
517 _okButton.setEnabled(inCardNum == 2 && ((MediaPreviewTableModel) _previewTable.getModel()).hasAnySelected());
521 * Enable or disable the edit boxes according to the radio button selections
523 private void enableEditBoxes()
525 // enable/disable text field for distance input
526 _limitDistBox.setEnabled(_distLimitRadio.isSelected());
527 // and for time limits
528 _limitMinBox.setEnabled(_timeLimitRadio.isSelected());
529 _limitSecBox.setEnabled(_timeLimitRadio.isSelected());
533 * Parse the time limit values entered and validate them
534 * @return TimeDifference object describing limit
536 protected TimeDifference parseTimeLimit()
538 if (!_timeLimitRadio.isSelected()) {return null;}
539 int mins = getValue(_limitMinBox.getText());
540 _limitMinBox.setText("" + mins);
541 int secs = getValue(_limitSecBox.getText());
542 _limitSecBox.setText("" + secs);
543 if (mins <= 0 && secs <= 0) {return null;}
544 return new TimeDifference(0, mins, secs, true);
548 * Parse the distance limit value entered and validate
549 * @return angular distance in radians
551 protected double parseDistanceLimit()
554 if (_distLimitRadio.isSelected())
557 value = Double.parseDouble(_limitDistBox.getText());
559 catch (NumberFormatException nfe) {}
562 _limitDistBox.setText("0");
565 _limitDistBox.setText("" + value);
566 return Distance.convertDistanceToRadians(value, getSelectedDistanceUnits());
571 * @return the selected distance units from the dropdown
573 protected Unit getSelectedDistanceUnits()
575 final Unit[] distUnits = {UnitSetLibrary.UNITS_KILOMETRES, UnitSetLibrary.UNITS_METRES, UnitSetLibrary.UNITS_MILES};
576 return distUnits[_distUnitsDropdown.getSelectedIndex()];
580 * Create a preview of the correlate action using the selected time difference
581 * @param inFromButton true if triggered from button press, false if automatic
583 public void createPreview(boolean inFromButton)
585 // Exit if still on first panel
586 if (!_previewEnabled) {
589 // Create a TimeDifference based on the edit boxes
590 int numHours = getValue(_offsetHourBox.getText());
591 int numMins = getValue(_offsetMinBox.getText());
592 int numSecs = getValue(_offsetSecBox.getText());
593 boolean isPos = _mediaLaterOption.isSelected();
594 createPreview(new TimeDifference(numHours, numMins, numSecs, isPos), inFromButton);
598 * Set up the final card using the given time difference and show it
599 * @param inTimeDiff time difference to use for time offsets
600 * @param inFirstMedia first media object to use for calculating timezone
602 protected void setupPreviewCard(TimeDifference inTimeDiff, MediaObject inFirstMedia)
604 _previewEnabled = false;
605 TimeDifference timeDiff = inTimeDiff;
606 if (timeDiff == null)
608 // No time difference available, so try with zero
609 timeDiff = new TimeDifference(0L);
611 // Use time difference to set edit boxes
612 _offsetHourBox.setText("" + timeDiff.getNumHours());
613 _offsetMinBox.setText("" + timeDiff.getNumMinutes());
614 _offsetSecBox.setText("" + timeDiff.getNumSeconds());
615 _mediaLaterOption.setSelected(timeDiff.getIsPositive());
616 _pointLaterOption.setSelected(!timeDiff.getIsPositive());
617 _previewEnabled = true;
619 createPreview(timeDiff, true);
623 * Create a preview of the correlate action using the selected time difference
624 * @param inTimeDiff TimeDifference to use for preview
625 * @param inShowWarning true to show warning if all points out of range
627 protected abstract void createPreview(TimeDifference inTimeDiff, boolean inShowWarning);
631 * Get the timestamp of the given media
632 * @param inMedia media object
633 * @return normally just returns the media timestamp, overridden by audio correlator
635 protected Timestamp getMediaTimestamp(MediaObject inMedia)
637 return inMedia.getTimestamp();
641 * Get the point pair surrounding the given media item
642 * @param inTrack track object
643 * @param inMedia media object
644 * @param inOffset time offset to apply
645 * @return point pair resulting from correlation
647 protected PointMediaPair getPointPairForMedia(Track inTrack, MediaObject inMedia, TimeDifference inOffset)
649 PointMediaPair pair = new PointMediaPair(inMedia);
650 if (inMedia.hasTimestamp())
652 // Add/subtract offset to media timestamp
653 Timestamp mediaStamp = getMediaTimestamp(inMedia);
654 int numPoints = inTrack.getNumPoints();
655 for (int i=0; i<numPoints; i++)
657 DataPoint point = inTrack.getPoint(i);
658 if (point.getPhoto() == null && point.getAudio() == null)
660 Timestamp pointStamp = point.getTimestamp();
661 if (pointStamp != null && pointStamp.isValid())
663 long numSeconds = pointStamp.getSecondsSince(mediaStamp, _timezone)
664 + inOffset.getTotalSeconds();
665 pair.addPoint(point, numSeconds);
675 * Finish the correlation
677 protected abstract void finishCorrelation();
680 * Construct an array of the point pairs to use for correlation
681 * @return array of PointMediaPair objects
683 protected PointMediaPair[] getPointPairs()
685 MediaPreviewTableModel model = (MediaPreviewTableModel) _previewTable.getModel();
686 int numMedia = model.getRowCount();
687 PointMediaPair[] pairs = new PointMediaPair[numMedia];
688 // Loop over items in preview table model
689 for (int i=0; i<numMedia; i++)
691 MediaPreviewTableRow row = model.getRow(i);
692 // add all selected pairs to array (other elements remain null)
693 if (row.getCorrelateFlag().booleanValue()) {
694 pairs[i] = row.getPointPair();