package tim.prune.correlate; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Calendar; import java.util.Iterator; import java.util.TreeSet; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import tim.prune.App; import tim.prune.GenericFunction; import tim.prune.I18nManager; import tim.prune.data.DataPoint; import tim.prune.data.Distance; import tim.prune.data.Field; import tim.prune.data.MediaObject; import tim.prune.data.MediaList; import tim.prune.data.TimeDifference; import tim.prune.data.Timestamp; import tim.prune.data.Track; import tim.prune.data.Unit; import tim.prune.data.UnitSetLibrary; import tim.prune.tips.TipManager; /** * Abstract superclass of the two correlator functions */ public abstract class Correlator extends GenericFunction { protected JDialog _dialog; private CardStack _cards = null; private JTable _selectionTable = null; protected JTable _previewTable = null; private boolean _previewEnabled = false; // flag required to enable preview function on final panel private boolean[] _cardEnabled = null; // flag for each card private JTextField _offsetHourBox = null, _offsetMinBox = null, _offsetSecBox = null; private JRadioButton _mediaLaterOption = null, _pointLaterOption = null; private JRadioButton _timeLimitRadio = null, _distLimitRadio = null; private JTextField _limitMinBox = null, _limitSecBox = null; private JTextField _limitDistBox = null; private JComboBox<String> _distUnitsDropdown = null; private JButton _nextButton = null, _backButton = null; protected JButton _okButton = null; /** * Constructor * @param inApp App object to report actions to */ public Correlator(App inApp) { super(inApp); } /** * @return type key eg photo, audio */ protected abstract String getMediaTypeKey(); /** * @return media list */ protected abstract MediaList getMediaList(); /** * Begin the function by initialising and showing the dialog */ public void begin() { // Check whether track has timestamps, exit if not if (!_app.getTrackInfo().getTrack().hasData(Field.TIMESTAMP)) { JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.correlate.notimestamps"), I18nManager.getText(getNameKey()), JOptionPane.INFORMATION_MESSAGE); return; } // Show warning if no uncorrelated audios if (!getMediaList().hasUncorrelatedMedia()) { Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")}; if (JOptionPane.showOptionDialog(_parentFrame, I18nManager.getText("dialog.correlate.nouncorrelated" + getMediaTypeKey() + "s"), I18nManager.getText(getNameKey()), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1]) == JOptionPane.NO_OPTION) { return; } } // Create dialog if necessary if (_dialog == null) { _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true); _dialog.setLocationRelativeTo(_parentFrame); _dialog.getContentPane().add(makeDialogContents()); _dialog.pack(); } // Go to first available card int card = 0; _cardEnabled = null; while (!isCardEnabled(card)) {card++;} _cards.showCard(card); showCard(0); // does set up and next/prev enabling _okButton.setEnabled(false); if (!isCardEnabled(1)) { _app.showTip(TipManager.Tip_ManuallyCorrelateOne); } _dialog.setVisible(true); } /** * Make contents of correlate dialog * @return JPanel containing gui elements */ private JPanel makeDialogContents() { JPanel mainPanel = new JPanel(); mainPanel.setLayout(new BorderLayout()); // Card panel in the middle _cards = new CardStack(); // First panel (not required by photo correlator) JPanel card1 = makeFirstPanel(); if (card1 == null) {card1 = new JPanel();} _cards.addCard(card1); // Second panel for selection of linked media _cards.addCard(makeSecondPanel()); // Third panel for options and preview _cards.addCard(makeThirdPanel()); mainPanel.add(_cards, BorderLayout.CENTER); // Button panel at the bottom JPanel buttonPanel = new JPanel(); _backButton = new JButton(I18nManager.getText("button.back")); _backButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { showCard(-1); } }); _backButton.setEnabled(false); buttonPanel.add(_backButton); _nextButton = new JButton(I18nManager.getText("button.next")); _nextButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { showCard(1); } }); buttonPanel.add(_nextButton); _okButton = new JButton(I18nManager.getText("button.ok")); _okButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { finishCorrelation(); _dialog.dispose(); } }); _okButton.setEnabled(false); buttonPanel.add(_okButton); JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); cancelButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { _dialog.dispose(); } }); buttonPanel.add(cancelButton); mainPanel.add(buttonPanel, BorderLayout.SOUTH); return mainPanel; } /** * Construct a table model for the photo / audio selection table * @return table model */ protected MediaSelectionTableModel makeSelectionTableModel() { MediaList mediaList = getMediaList(); MediaSelectionTableModel model = new MediaSelectionTableModel( "dialog.correlate.select." + getMediaTypeKey() + "name", "dialog.correlate.select." + getMediaTypeKey() + "later"); int numMedia = mediaList.getNumMedia(); for (int i=0; i<numMedia; i++) { MediaObject media = mediaList.getMedia(i); // For working out time differences, can't use media which already had point information if (media.getDataPoint() != null && media.getDataPoint().hasTimestamp() && media.getOriginalStatus() == MediaObject.Status.NOT_CONNECTED) { // Calculate time difference, add to table model long timeDiff = getMediaTimestamp(media).getSecondsSince(media.getDataPoint().getTimestamp()); model.addMedia(media, timeDiff); } } return model; } /** * Group the two radio buttons together with a ButtonGroup * @param inButton1 first radio button * @param inButton2 second radio button */ protected static void groupRadioButtons(JRadioButton inButton1, JRadioButton inButton2) { ButtonGroup buttonGroup = new ButtonGroup(); buttonGroup.add(inButton1); buttonGroup.add(inButton2); inButton1.setSelected(true); } /** * Try to parse the given string * @param inText String to parse * @return value if parseable, 0 otherwise */ protected static int getValue(String inText) { int value = 0; try { value = Integer.parseInt(inText); } catch (NumberFormatException nfe) {} return value; } /** * @param inFirstTimestamp timestamp of first photo / audio object, or null if not available * @return time difference of local time zone from UTC when the first photo was taken */ private static TimeDifference getTimezoneOffset(Timestamp inFirstTimestamp) { Calendar cal = null; // Use first timestamp if available if (inFirstTimestamp != null) { cal = inFirstTimestamp.getCalendar(); } else { // No photo or no timestamp, just use current time cal = Calendar.getInstance(); } // Both time zone offset and dst offset are based on milliseconds, so convert to seconds TimeDifference timeDiff = new TimeDifference((cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 1000); return timeDiff; } /** * Calculate the median index to select from the table * @param inModel table model * @return index of entry to select from table */ protected static int getMedianIndex(MediaSelectionTableModel inModel) { // make sortable list TreeSet<TimeIndexPair> set = new TreeSet<TimeIndexPair>(); // loop through rows of table adding to list int numRows = inModel.getRowCount(); int i; for (i=0; i<numRows; i++) { MediaSelectionTableRow row = inModel.getRow(i); set.add(new TimeIndexPair(row.getTimeDiff().getTotalSeconds(), i)); } // pull out middle entry and return index TimeIndexPair pair = null; Iterator<TimeIndexPair> iterator = set.iterator(); for (i=0; i<(numRows+1)/2; i++) { pair = iterator.next(); } return pair.getIndex(); } /** * Disable the ok button */ public void disableOkButton() { if (_okButton != null) { _okButton.setEnabled(false); } } /** * @return gui components for first panel, or null if empty */ protected JPanel makeFirstPanel() { return null; } /** * Make the second panel for the selection screen * @return JPanel object containing gui elements */ private JPanel makeSecondPanel() { JPanel card = new JPanel(); card.setLayout(new BorderLayout(10, 10)); JLabel introLabel = new JLabel(I18nManager.getText( "dialog.correlate." + getMediaTypeKey() + "select.intro")); introLabel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6)); card.add(introLabel, BorderLayout.NORTH); // table doesn't have model yet - that will be attached later _selectionTable = new JTable(); JScrollPane photoScrollPane = new JScrollPane(_selectionTable); photoScrollPane.setPreferredSize(new Dimension(400, 100)); card.add(photoScrollPane, BorderLayout.CENTER); return card; } /** * Make contents of third panel including options and preview * @return JPanel containing gui elements */ private JPanel makeThirdPanel() { OptionsChangedListener optionsChangedListener = new OptionsChangedListener(this); // Second panel for options JPanel card2 = new JPanel(); card2.setLayout(new BorderLayout()); JPanel card2Top = new JPanel(); card2Top.setLayout(new BoxLayout(card2Top, BoxLayout.Y_AXIS)); JLabel introLabel = new JLabel(I18nManager.getText("dialog.correlate.options.intro")); introLabel.setBorder(BorderFactory.createEmptyBorder(8, 6, 5, 6)); card2Top.add(introLabel); // time offset section JPanel offsetPanel = new JPanel(); offsetPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.offsetpanel"))); offsetPanel.setLayout(new BoxLayout(offsetPanel, BoxLayout.Y_AXIS)); JPanel offsetPanelTop = new JPanel(); offsetPanelTop.setLayout(new FlowLayout()); offsetPanelTop.setBorder(null); offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset") + ": ")); _offsetHourBox = new JTextField(3); _offsetHourBox.addKeyListener(optionsChangedListener); offsetPanelTop.add(_offsetHourBox); offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.hours"))); _offsetMinBox = new JTextField(3); _offsetMinBox.addKeyListener(optionsChangedListener); offsetPanelTop.add(_offsetMinBox); offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes"))); _offsetSecBox = new JTextField(3); _offsetSecBox.addKeyListener(optionsChangedListener); offsetPanelTop.add(_offsetSecBox); offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds"))); offsetPanel.add(offsetPanelTop); // radio buttons for photo / point later JPanel offsetPanelBot = new JPanel(); offsetPanelBot.setLayout(new FlowLayout()); offsetPanelBot.setBorder(null); _mediaLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options." + getMediaTypeKey() + "later")); _pointLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.pointlater" + getMediaTypeKey())); _mediaLaterOption.addItemListener(optionsChangedListener); _pointLaterOption.addItemListener(optionsChangedListener); ButtonGroup laterGroup = new ButtonGroup(); laterGroup.add(_mediaLaterOption); laterGroup.add(_pointLaterOption); offsetPanelBot.add(_mediaLaterOption); offsetPanelBot.add(_pointLaterOption); offsetPanel.add(offsetPanelBot); offsetPanel.setAlignmentX(Component.LEFT_ALIGNMENT); card2Top.add(offsetPanel); // listener for radio buttons ActionListener radioListener = new ActionListener() { public void actionPerformed(ActionEvent e) { enableEditBoxes(); } }; // time limits section JPanel limitsPanel = new JPanel(); limitsPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.limitspanel"))); limitsPanel.setLayout(new BoxLayout(limitsPanel, BoxLayout.Y_AXIS)); JPanel timeLimitPanel = new JPanel(); timeLimitPanel.setLayout(new FlowLayout()); JRadioButton noTimeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.notimelimit")); noTimeLimitRadio.addItemListener(optionsChangedListener); noTimeLimitRadio.addActionListener(radioListener); timeLimitPanel.add(noTimeLimitRadio); _timeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.timelimit") + ": "); _timeLimitRadio.addItemListener(optionsChangedListener); _timeLimitRadio.addActionListener(radioListener); timeLimitPanel.add(_timeLimitRadio); groupRadioButtons(noTimeLimitRadio, _timeLimitRadio); _limitMinBox = new JTextField(3); _limitMinBox.addKeyListener(optionsChangedListener); timeLimitPanel.add(_limitMinBox); timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes"))); _limitSecBox = new JTextField(3); _limitSecBox.addKeyListener(optionsChangedListener); timeLimitPanel.add(_limitSecBox); timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds"))); limitsPanel.add(timeLimitPanel); // distance limits JPanel distLimitPanel = new JPanel(); distLimitPanel.setLayout(new FlowLayout()); JRadioButton noDistLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.nodistancelimit")); noDistLimitRadio.addItemListener(optionsChangedListener); noDistLimitRadio.addActionListener(radioListener); distLimitPanel.add(noDistLimitRadio); _distLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.distancelimit") + ": "); _distLimitRadio.addItemListener(optionsChangedListener); _distLimitRadio.addActionListener(radioListener); distLimitPanel.add(_distLimitRadio); groupRadioButtons(noDistLimitRadio, _distLimitRadio); _limitDistBox = new JTextField(4); _limitDistBox.addKeyListener(optionsChangedListener); distLimitPanel.add(_limitDistBox); String[] distUnitsOptions = {I18nManager.getText("units.kilometres"), I18nManager.getText("units.metres"), I18nManager.getText("units.miles")}; _distUnitsDropdown = new JComboBox<String>(distUnitsOptions); _distUnitsDropdown.addItemListener(optionsChangedListener); distLimitPanel.add(_distUnitsDropdown); limitsPanel.add(distLimitPanel); limitsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); card2Top.add(limitsPanel); // preview button JButton previewButton = new JButton(I18nManager.getText("button.preview")); previewButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { createPreview(true); } }); card2Top.add(previewButton); card2.add(card2Top, BorderLayout.NORTH); // preview _previewTable = new JTable(new MediaPreviewTableModel("dialog.correlate.select." + getMediaTypeKey() + "name")); JScrollPane previewScrollPane = new JScrollPane(_previewTable); previewScrollPane.setPreferredSize(new Dimension(300, 100)); card2.add(previewScrollPane, BorderLayout.CENTER); return card2; } /** * Go to the next or previous card in the stack * @param increment 1 for next, -1 for previous card */ private void showCard(int increment) { int currCard = _cards.getCurrentCardIndex(); int next = currCard + increment; if (!isCardEnabled(next)) { next += increment; } setupCard(next); _backButton.setEnabled(next > 0 && (isCardEnabled(next-1) || isCardEnabled(next-2))); _nextButton.setEnabled(next < (_cards.getNumCards()-1)); _cards.showCard(next); } /** * @param inCardNum index of card * @return true if specified card is enabled */ private boolean isCardEnabled(int inCardNum) { if (_cardEnabled == null) {_cardEnabled = getCardEnabledFlags();} return (inCardNum >= 0 && inCardNum < _cardEnabled.length && _cardEnabled[inCardNum]); } /** * @return array of boolean flags denoting availability of cards */ protected boolean[] getCardEnabledFlags() { // by default first is off and third is always on; second depends on selection table return new boolean[] {false, makeSelectionTableModel().getRowCount() > 0, true}; } /** * Set up the specified card * @param inCardNum index of card */ protected void setupCard(int inCardNum) { _previewEnabled = false; if (inCardNum == 1) { // set up photo selection card MediaSelectionTableModel model = makeSelectionTableModel(); _selectionTable.setModel(model); for (int i=0; i<model.getColumnCount(); i++) { _selectionTable.getColumnModel().getColumn(i).setPreferredWidth(i==3?50:150); } // Calculate median time difference, select corresponding row of table int preselectedIndex = model.getRowCount() < 3 ? 0 : getMedianIndex(model); _selectionTable.getSelectionModel().setSelectionInterval(preselectedIndex, preselectedIndex); _nextButton.requestFocus(); } else if (inCardNum == 2) { // set up the options/preview card - first check for given time difference TimeDifference timeDiff = null; if (isCardEnabled(1)) { int rowNum = _selectionTable.getSelectedRow(); if (rowNum < 0) {rowNum = 0;} MediaSelectionTableRow selectedRow = ((MediaSelectionTableModel) _selectionTable.getModel()).getRow(rowNum); timeDiff = selectedRow.getTimeDiff(); } setupPreviewCard(timeDiff, getMediaList().getMedia(0)); } // enable ok button if any photos have been selected _okButton.setEnabled(inCardNum == 2 && ((MediaPreviewTableModel) _previewTable.getModel()).hasAnySelected()); } /** * Enable or disable the edit boxes according to the radio button selections */ private void enableEditBoxes() { // enable/disable text field for distance input _limitDistBox.setEnabled(_distLimitRadio.isSelected()); // and for time limits _limitMinBox.setEnabled(_timeLimitRadio.isSelected()); _limitSecBox.setEnabled(_timeLimitRadio.isSelected()); } /** * Parse the time limit values entered and validate them * @return TimeDifference object describing limit */ protected TimeDifference parseTimeLimit() { if (!_timeLimitRadio.isSelected()) {return null;} int mins = getValue(_limitMinBox.getText()); _limitMinBox.setText("" + mins); int secs = getValue(_limitSecBox.getText()); _limitSecBox.setText("" + secs); if (mins <= 0 && secs <= 0) {return null;} return new TimeDifference(0, mins, secs, true); } /** * Parse the distance limit value entered and validate * @return angular distance in radians */ protected double parseDistanceLimit() { double value = -1.0; if (_distLimitRadio.isSelected()) { try { value = Double.parseDouble(_limitDistBox.getText()); } catch (NumberFormatException nfe) {} } if (value <= 0.0) { _limitDistBox.setText("0"); return -1.0; } _limitDistBox.setText("" + value); return Distance.convertDistanceToRadians(value, getSelectedDistanceUnits()); } /** * @return the selected distance units from the dropdown */ protected Unit getSelectedDistanceUnits() { final Unit[] distUnits = {UnitSetLibrary.UNITS_KILOMETRES, UnitSetLibrary.UNITS_METRES, UnitSetLibrary.UNITS_MILES}; return distUnits[_distUnitsDropdown.getSelectedIndex()]; } /** * Create a preview of the correlate action using the selected time difference * @param inFromButton true if triggered from button press, false if automatic */ public void createPreview(boolean inFromButton) { // Exit if still on first panel if (!_previewEnabled) {return;} // Create a TimeDifference based on the edit boxes int numHours = getValue(_offsetHourBox.getText()); int numMins = getValue(_offsetMinBox.getText()); int numSecs = getValue(_offsetSecBox.getText()); boolean isPos = _mediaLaterOption.isSelected(); createPreview(new TimeDifference(numHours, numMins, numSecs, isPos), inFromButton); } /** * Set up the final card using the given time difference and show it * @param inTimeDiff time difference to use for time offsets * @param inFirstMedia first media object to use for calculating timezone */ protected void setupPreviewCard(TimeDifference inTimeDiff, MediaObject inFirstMedia) { _previewEnabled = false; TimeDifference timeDiff = inTimeDiff; if (timeDiff == null) { // No time difference available, so calculate based on computer's time zone Timestamp tstamp = null; if (inFirstMedia != null) { tstamp = inFirstMedia.getTimestamp(); } timeDiff = getTimezoneOffset(tstamp); } // Use time difference to set edit boxes _offsetHourBox.setText("" + timeDiff.getNumHours()); _offsetMinBox.setText("" + timeDiff.getNumMinutes()); _offsetSecBox.setText("" + timeDiff.getNumSeconds()); _mediaLaterOption.setSelected(timeDiff.getIsPositive()); _pointLaterOption.setSelected(!timeDiff.getIsPositive()); _previewEnabled = true; enableEditBoxes(); createPreview(timeDiff, true); } /** * Create a preview of the correlate action using the selected time difference * @param inTimeDiff TimeDifference to use for preview * @param inShowWarning true to show warning if all points out of range */ protected abstract void createPreview(TimeDifference inTimeDiff, boolean inShowWarning); /** * Get the timestamp of the given media * @param inMedia media object * @return normally just returns the media timestamp, overridden by audio correlator */ protected Timestamp getMediaTimestamp(MediaObject inMedia) { return inMedia.getTimestamp(); } /** * Get the point pair surrounding the given media item * @param inTrack track object * @param inMedia media object * @param inOffset time offset to apply * @return point pair resulting from correlation */ protected PointMediaPair getPointPairForMedia(Track inTrack, MediaObject inMedia, TimeDifference inOffset) { PointMediaPair pair = new PointMediaPair(inMedia); if (inMedia.hasTimestamp()) { // Add/subtract offset to media timestamp Timestamp mediaStamp = getMediaTimestamp(inMedia).createMinusOffset(inOffset); int numPoints = inTrack.getNumPoints(); for (int i=0; i<numPoints; i++) { DataPoint point = inTrack.getPoint(i); if (point.getPhoto() == null && point.getAudio() == null) { Timestamp pointStamp = point.getTimestamp(); if (pointStamp != null && pointStamp.isValid()) { long numSeconds = pointStamp.getSecondsSince(mediaStamp); pair.addPoint(point, numSeconds); } } } } return pair; } /** * Finish the correlation */ protected abstract void finishCorrelation(); /** * Construct an array of the point pairs to use for correlation * @return array of PointMediaPair objects */ protected PointMediaPair[] getPointPairs() { MediaPreviewTableModel model = (MediaPreviewTableModel) _previewTable.getModel(); int numMedia = model.getRowCount(); PointMediaPair[] pairs = new PointMediaPair[numMedia]; // Loop over items in preview table model for (int i=0; i<numMedia; i++) { MediaPreviewTableRow row = model.getRow(i); // add all selected pairs to array (other elements remain null) if (row.getCorrelateFlag().booleanValue()) { pairs[i] = row.getPointPair(); } } return pairs; } }