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.Calendar;
10 import java.util.Iterator;
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.data.DataPoint;
31 import tim.prune.data.Distance;
32 import tim.prune.data.Field;
33 import tim.prune.data.MediaFile;
34 import tim.prune.data.MediaList;
35 import tim.prune.data.TimeDifference;
36 import tim.prune.data.Timestamp;
37 import tim.prune.data.Track;
40 * Abstract superclass of the two correlator functions
42 public abstract class Correlator extends GenericFunction
44 protected JDialog _dialog;
45 private CardStack _cards = null;
46 private JLabel _tipLabel = null;
47 private JTable _selectionTable = null;
48 protected JTable _previewTable = null;
49 private boolean _previewEnabled = false; // flag required to enable preview function on final panel
50 private boolean[] _cardEnabled = null; // flag for each card
51 private JTextField _offsetHourBox = null, _offsetMinBox = null, _offsetSecBox = null;
52 private JRadioButton _mediaLaterOption = null, _pointLaterOption = null;
53 private JRadioButton _timeLimitRadio = null, _distLimitRadio = null;
54 private JTextField _limitMinBox = null, _limitSecBox = null;
55 private JTextField _limitDistBox = null;
56 private JComboBox _distUnitsDropdown = null;
57 private JButton _nextButton = null, _backButton = null;
58 protected JButton _okButton = null;
62 * @param inApp App object to report actions to
64 public Correlator(App inApp) {
69 * @return type key eg photo, audio
71 protected abstract String getMediaTypeKey();
76 protected abstract MediaList getMediaList();
79 * Begin the function by initialising and showing the dialog
83 // Check whether track has timestamps, exit if not
84 if (!_app.getTrackInfo().getTrack().hasData(Field.TIMESTAMP))
86 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.correlate.notimestamps"),
87 I18nManager.getText(getNameKey()), JOptionPane.INFORMATION_MESSAGE);
90 // Show warning if no uncorrelated audios
91 if (!getMediaList().hasUncorrelatedMedia())
93 Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")};
94 if (JOptionPane.showOptionDialog(_parentFrame,
95 I18nManager.getText("dialog.correlate.nouncorrelated" + getMediaTypeKey() + "s"),
96 I18nManager.getText(getNameKey()), JOptionPane.YES_NO_OPTION,
97 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
98 == JOptionPane.NO_OPTION)
103 // Create dialog if necessary
106 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
107 _dialog.setLocationRelativeTo(_parentFrame);
108 _dialog.getContentPane().add(makeDialogContents());
111 // Go to first available card
114 while (!isCardEnabled(card)) {card++;}
115 _cards.showCard(card);
116 showCard(0); // does set up and next/prev enabling
117 _okButton.setEnabled(false);
118 _dialog.setVisible(true);
122 * Make contents of correlate dialog
123 * @return JPanel containing gui elements
125 private JPanel makeDialogContents()
127 JPanel mainPanel = new JPanel();
128 mainPanel.setLayout(new BorderLayout());
130 // Card panel in the middle
131 _cards = new CardStack();
133 // First panel (not required by photo correlator)
134 JPanel card1 = makeFirstPanel();
135 if (card1 == null) {card1 = new JPanel();}
136 _cards.addCard(card1);
138 // Second panel for selection of linked media
139 _cards.addCard(makeSecondPanel());
141 // Third panel for options and preview
142 _cards.addCard(makeThirdPanel());
143 mainPanel.add(_cards, BorderLayout.CENTER);
145 // Button panel at the bottom
146 JPanel buttonPanel = new JPanel();
147 _backButton = new JButton(I18nManager.getText("button.back"));
148 _backButton.addActionListener(new ActionListener() {
149 public void actionPerformed(ActionEvent e) {
153 _backButton.setEnabled(false);
154 buttonPanel.add(_backButton);
155 _nextButton = new JButton(I18nManager.getText("button.next"));
156 _nextButton.addActionListener(new ActionListener() {
157 public void actionPerformed(ActionEvent e) {
161 buttonPanel.add(_nextButton);
162 _okButton = new JButton(I18nManager.getText("button.ok"));
163 _okButton.addActionListener(new ActionListener()
165 public void actionPerformed(ActionEvent e)
171 _okButton.setEnabled(false);
172 buttonPanel.add(_okButton);
173 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
174 cancelButton.addActionListener(new ActionListener() {
175 public void actionPerformed(ActionEvent e) {
179 buttonPanel.add(cancelButton);
180 mainPanel.add(buttonPanel, BorderLayout.SOUTH);
185 * Construct a table model for the photo / audio selection table
186 * @return table model
188 protected MediaSelectionTableModel makeSelectionTableModel()
190 MediaList mediaList = getMediaList();
191 MediaSelectionTableModel model = new MediaSelectionTableModel(
192 "dialog.correlate.select." + getMediaTypeKey() + "name",
193 "dialog.correlate.select." + getMediaTypeKey() + "later");
194 int numMedia = mediaList.getNumMedia();
195 for (int i=0; i<numMedia; i++)
197 MediaFile media = mediaList.getMedia(i);
198 // For working out time differences, can't use media which already had point information
199 if (media.getDataPoint() != null && media.getDataPoint().hasTimestamp()
200 && media.getOriginalStatus() == MediaFile.Status.NOT_CONNECTED)
202 // Calculate time difference, add to table model
203 long timeDiff = getMediaTimestamp(media).getSecondsSince(media.getDataPoint().getTimestamp());
204 model.addMedia(media, timeDiff);
211 * Group the two radio buttons together with a ButtonGroup
212 * @param inButton1 first radio button
213 * @param inButton2 second radio button
215 protected static void groupRadioButtons(JRadioButton inButton1, JRadioButton inButton2)
217 ButtonGroup buttonGroup = new ButtonGroup();
218 buttonGroup.add(inButton1);
219 buttonGroup.add(inButton2);
220 inButton1.setSelected(true);
225 * Try to parse the given string
226 * @param inText String to parse
227 * @return value if parseable, 0 otherwise
229 protected static int getValue(String inText)
233 value = Integer.parseInt(inText);
235 catch (NumberFormatException nfe) {}
241 * @param inFirstTimestamp timestamp of first photo / audio object, or null if not available
242 * @return time difference of local time zone from UTC when the first photo was taken
244 private static TimeDifference getTimezoneOffset(Timestamp inFirstTimestamp)
247 // Use first timestamp if available
248 if (inFirstTimestamp != null) {
249 cal = inFirstTimestamp.getCalendar();
252 // No photo or no timestamp, just use current time
253 cal = Calendar.getInstance();
255 // Both time zone offset and dst offset are based on milliseconds, so convert to seconds
256 TimeDifference timeDiff = new TimeDifference((cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 1000);
262 * Calculate the median index to select from the table
263 * @param inModel table model
264 * @return index of entry to select from table
266 protected static int getMedianIndex(MediaSelectionTableModel inModel)
268 // make sortable list
269 TreeSet<TimeIndexPair> set = new TreeSet<TimeIndexPair>();
270 // loop through rows of table adding to list
271 int numRows = inModel.getRowCount();
273 for (i=0; i<numRows; i++)
275 MediaSelectionTableRow row = inModel.getRow(i);
276 set.add(new TimeIndexPair(row.getTimeDiff().getTotalSeconds(), i));
278 // pull out middle entry and return index
279 TimeIndexPair pair = null;
280 Iterator<TimeIndexPair> iterator = set.iterator();
281 for (i=0; i<(numRows+1)/2; i++)
283 pair = iterator.next();
285 return pair.getIndex();
290 * Disable the ok button
292 public void disableOkButton()
294 if (_okButton != null) {
295 _okButton.setEnabled(false);
300 * @return gui components for first panel, or null if empty
302 protected JPanel makeFirstPanel() {
307 * Make the second panel for the selection screen
308 * @return JPanel object containing gui elements
310 private JPanel makeSecondPanel()
312 JPanel card = new JPanel();
313 card.setLayout(new BorderLayout(10, 10));
314 card.add(new JLabel(I18nManager.getText(
315 "dialog.correlate." + getMediaTypeKey() + "select.intro")), BorderLayout.NORTH);
316 // table doesn't have model yet - that will be attached later
317 _selectionTable = new JTable();
318 JScrollPane photoScrollPane = new JScrollPane(_selectionTable);
319 photoScrollPane.setPreferredSize(new Dimension(400, 100));
320 card.add(photoScrollPane, BorderLayout.CENTER);
326 * Make contents of third panel including options and preview
327 * @return JPanel containing gui elements
329 private JPanel makeThirdPanel()
331 OptionsChangedListener optionsChangedListener = new OptionsChangedListener(this);
332 // Second panel for options
333 JPanel card2 = new JPanel();
334 card2.setLayout(new BorderLayout());
335 JPanel card2Top = new JPanel();
336 card2Top.setLayout(new BoxLayout(card2Top, BoxLayout.Y_AXIS));
337 _tipLabel = new JLabel(I18nManager.getText("dialog.correlate.options.tip"));
338 _tipLabel.setBorder(BorderFactory.createEmptyBorder(8, 6, 5, 6));
339 card2Top.add(_tipLabel);
340 JLabel introLabel = new JLabel(I18nManager.getText("dialog.correlate.options.intro"));
341 introLabel.setBorder(BorderFactory.createEmptyBorder(8, 6, 5, 6));
342 card2Top.add(introLabel);
343 // time offset section
344 JPanel offsetPanel = new JPanel();
345 offsetPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.offsetpanel")));
346 offsetPanel.setLayout(new BoxLayout(offsetPanel, BoxLayout.Y_AXIS));
347 JPanel offsetPanelTop = new JPanel();
348 offsetPanelTop.setLayout(new FlowLayout());
349 offsetPanelTop.setBorder(null);
350 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset") + ": "));
351 _offsetHourBox = new JTextField(3);
352 _offsetHourBox.addKeyListener(optionsChangedListener);
353 offsetPanelTop.add(_offsetHourBox);
354 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.hours")));
355 _offsetMinBox = new JTextField(3);
356 _offsetMinBox.addKeyListener(optionsChangedListener);
357 offsetPanelTop.add(_offsetMinBox);
358 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
359 _offsetSecBox = new JTextField(3);
360 _offsetSecBox.addKeyListener(optionsChangedListener);
361 offsetPanelTop.add(_offsetSecBox);
362 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
363 offsetPanel.add(offsetPanelTop);
365 // radio buttons for photo / point later
366 JPanel offsetPanelBot = new JPanel();
367 offsetPanelBot.setLayout(new FlowLayout());
368 offsetPanelBot.setBorder(null);
369 _mediaLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options." + getMediaTypeKey() + "later"));
370 _pointLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.pointlaterphoto"));
371 _mediaLaterOption.addItemListener(optionsChangedListener);
372 _pointLaterOption.addItemListener(optionsChangedListener);
373 ButtonGroup laterGroup = new ButtonGroup();
374 laterGroup.add(_mediaLaterOption);
375 laterGroup.add(_pointLaterOption);
376 offsetPanelBot.add(_mediaLaterOption);
377 offsetPanelBot.add(_pointLaterOption);
378 offsetPanel.add(offsetPanelBot);
379 offsetPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
380 card2Top.add(offsetPanel);
382 // time limits section
383 JPanel limitsPanel = new JPanel();
384 limitsPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.limitspanel")));
385 limitsPanel.setLayout(new BoxLayout(limitsPanel, BoxLayout.Y_AXIS));
386 JPanel timeLimitPanel = new JPanel();
387 timeLimitPanel.setLayout(new FlowLayout());
388 JRadioButton noTimeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.notimelimit"));
389 noTimeLimitRadio.addItemListener(optionsChangedListener);
390 timeLimitPanel.add(noTimeLimitRadio);
391 _timeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.timelimit") + " : ");
392 _timeLimitRadio.addItemListener(optionsChangedListener);
393 timeLimitPanel.add(_timeLimitRadio);
394 groupRadioButtons(noTimeLimitRadio, _timeLimitRadio);
395 _limitMinBox = new JTextField(3);
396 _limitMinBox.addKeyListener(optionsChangedListener);
397 timeLimitPanel.add(_limitMinBox);
398 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
399 _limitSecBox = new JTextField(3);
400 _limitSecBox.addKeyListener(optionsChangedListener);
401 timeLimitPanel.add(_limitSecBox);
402 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
403 limitsPanel.add(timeLimitPanel);
405 JPanel distLimitPanel = new JPanel();
406 distLimitPanel.setLayout(new FlowLayout());
407 JRadioButton noDistLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.nodistancelimit"));
408 noDistLimitRadio.addItemListener(optionsChangedListener);
409 distLimitPanel.add(noDistLimitRadio);
410 _distLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.distancelimit"));
411 _distLimitRadio.addItemListener(optionsChangedListener);
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(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) {_cardEnabled = getCardEnabledFlags();}
468 return (inCardNum >= 0 && inCardNum < _cardEnabled.length && _cardEnabled[inCardNum]);
472 * @return array of boolean flags denoting availability of cards
474 protected boolean[] getCardEnabledFlags()
476 // by default first is off and third is always on; second depends on selection table
477 return new boolean[] {false, makeSelectionTableModel().getRowCount() > 0, true};
481 * Set up the specified card
482 * @param inCardNum index of card
484 protected void setupCard(int inCardNum)
486 _previewEnabled = false;
489 // set up photo selection card
490 MediaSelectionTableModel model = makeSelectionTableModel();
491 _selectionTable.setModel(model);
492 for (int i=0; i<model.getColumnCount(); i++) {
493 _selectionTable.getColumnModel().getColumn(i).setPreferredWidth(i==3?50:150);
495 // Calculate median time difference, select corresponding row of table
496 int preselectedIndex = model.getRowCount() < 3 ? 0 : getMedianIndex(model);
497 _selectionTable.getSelectionModel().setSelectionInterval(preselectedIndex, preselectedIndex);
498 _nextButton.requestFocus();
500 else if (inCardNum == 2)
502 // set up the options/preview card - first check for given time difference
503 TimeDifference timeDiff = null;
504 if (isCardEnabled(1))
506 int rowNum = _selectionTable.getSelectedRow();
507 if (rowNum < 0) {rowNum = 0;}
508 MediaSelectionTableRow selectedRow =
509 ((MediaSelectionTableModel) _selectionTable.getModel()).getRow(rowNum);
510 timeDiff = selectedRow.getTimeDiff();
512 setupPreviewCard(timeDiff, getMediaList().getMedia(0));
514 // enable ok button if any photos have been selected
515 _okButton.setEnabled(inCardNum == 2 && ((MediaPreviewTableModel) _previewTable.getModel()).hasAnySelected());
519 * Parse the time limit values entered and validate them
520 * @return TimeDifference object describing limit
522 protected TimeDifference parseTimeLimit()
524 if (!_timeLimitRadio.isSelected()) {return null;}
525 int mins = getValue(_limitMinBox.getText());
526 _limitMinBox.setText("" + mins);
527 int secs = getValue(_limitSecBox.getText());
528 _limitSecBox.setText("" + secs);
529 if (mins <= 0 && secs <= 0) {return null;}
530 return new TimeDifference(0, mins, secs, true);
534 * Parse the distance limit value entered and validate
535 * @return angular distance in radians
537 protected double parseDistanceLimit()
540 if (_distLimitRadio.isSelected())
543 value = Double.parseDouble(_limitDistBox.getText());
545 catch (NumberFormatException nfe) {}
548 _limitDistBox.setText("0");
551 _limitDistBox.setText("" + value);
552 return Distance.convertDistanceToRadians(value, getSelectedDistanceUnits());
557 * @return the selected distance units from the dropdown
559 protected Distance.Units getSelectedDistanceUnits()
561 final Distance.Units[] distUnits = {Distance.Units.KILOMETRES, Distance.Units.METRES, Distance.Units.MILES};
562 return distUnits[_distUnitsDropdown.getSelectedIndex()];
566 * Create a preview of the correlate action using the selected time difference
567 * @param inFromButton true if triggered from button press, false if automatic
569 public void createPreview(boolean inFromButton)
571 // Exit if still on first panel
572 if (!_previewEnabled) {return;}
573 // Create a TimeDifference based on the edit boxes
574 int numHours = getValue(_offsetHourBox.getText());
575 int numMins = getValue(_offsetMinBox.getText());
576 int numSecs = getValue(_offsetSecBox.getText());
577 boolean isPos = _mediaLaterOption.isSelected();
578 createPreview(new TimeDifference(numHours, numMins, numSecs, isPos), inFromButton);
582 * Set up the final card using the given time difference and show it
583 * @param inTimeDiff time difference to use for time offsets
584 * @param inFirstMedia first media object to use for calculating timezone
586 protected void setupPreviewCard(TimeDifference inTimeDiff, MediaFile inFirstMedia)
588 _previewEnabled = false;
589 TimeDifference timeDiff = inTimeDiff;
590 if (timeDiff == null)
592 // No time difference available, so calculate based on computer's time zone
593 Timestamp tstamp = null;
594 if (inFirstMedia != null) {
595 tstamp = inFirstMedia.getTimestamp();
597 timeDiff = getTimezoneOffset(tstamp);
599 // Use time difference to set edit boxes
600 _offsetHourBox.setText("" + timeDiff.getNumHours());
601 _offsetMinBox.setText("" + timeDiff.getNumMinutes());
602 _offsetSecBox.setText("" + timeDiff.getNumSeconds());
603 _mediaLaterOption.setSelected(timeDiff.getIsPositive());
604 _pointLaterOption.setSelected(!timeDiff.getIsPositive());
605 _previewEnabled = true;
606 createPreview(timeDiff, true);
610 * Create a preview of the correlate action using the selected time difference
611 * @param inTimeDiff TimeDifference to use for preview
612 * @param inShowWarning true to show warning if all points out of range
614 protected abstract void createPreview(TimeDifference inTimeDiff, boolean inShowWarning);
618 * Get the timestamp of the given media
619 * @param inMedia media object
620 * @return normally just returns the media timestamp, overridden by audio correlator
622 protected Timestamp getMediaTimestamp(MediaFile inMedia)
624 return inMedia.getTimestamp();
628 * Get the point pair surrounding the given media item
629 * @param inTrack track object
630 * @param inMedia media object
631 * @param inOffset time offset to apply
632 * @return point pair resulting from correlation
634 protected PointMediaPair getPointPairForMedia(Track inTrack, MediaFile inMedia, TimeDifference inOffset)
636 PointMediaPair pair = new PointMediaPair(inMedia);
637 // Add/subtract offset to media timestamp
638 Timestamp mediaStamp = getMediaTimestamp(inMedia).createMinusOffset(inOffset);
639 int numPoints = inTrack.getNumPoints();
640 for (int i=0; i<numPoints; i++)
642 DataPoint point = inTrack.getPoint(i);
643 if (point.getPhoto() == null && point.getAudio() == null)
645 Timestamp pointStamp = point.getTimestamp();
646 if (pointStamp != null && pointStamp.isValid())
648 long numSeconds = pointStamp.getSecondsSince(mediaStamp);
649 pair.addPoint(point, numSeconds);
658 * Finish the correlation
660 protected abstract void finishCorrelation();
663 * Construct an array of the point pairs to use for correlation
664 * @return array of PointMediaPair objects
666 protected PointMediaPair[] getPointPairs()
668 MediaPreviewTableModel model = (MediaPreviewTableModel) _previewTable.getModel();
669 int numMedia = model.getRowCount();
670 PointMediaPair[] pairs = new PointMediaPair[numMedia];
671 // Loop over items in preview table model
672 for (int i=0; i<numMedia; i++)
674 MediaPreviewTableRow row = model.getRow(i);
675 // add all selected pairs to array (other elements remain null)
676 if (row.getCorrelateFlag().booleanValue()) {
677 pairs[i] = row.getPointPair();