]> gitweb.fperrin.net Git - GpsPrune.git/blobdiff - tim/prune/correlate/PhotoCorrelator.java
Version 12, December 2010
[GpsPrune.git] / tim / prune / correlate / PhotoCorrelator.java
index 84c179ec3d3a23576d9e6ee83cdf1133758e20af..f5f86b1ab36982565a44d9cac29d9bd7aff7ee46 100644 (file)
@@ -1,73 +1,30 @@
 package tim.prune.correlate;
 
-import java.awt.BorderLayout;
-import java.awt.CardLayout;
-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.DataSubscriber;
 import tim.prune.I18nManager;
+import tim.prune.UpdateMessageBroker;
 import tim.prune.data.DataPoint;
-import tim.prune.data.Distance;
-import tim.prune.data.Field;
+import tim.prune.data.MediaList;
 import tim.prune.data.Photo;
 import tim.prune.data.PhotoList;
 import tim.prune.data.TimeDifference;
-import tim.prune.data.Timestamp;
-import tim.prune.data.Track;
-import tim.prune.data.TrackInfo;
 import tim.prune.undo.UndoCorrelatePhotos;
 
 /**
  * Class to manage the automatic correlation of photos to points
  * including the GUI stuff to control the correlation options
  */
-public class PhotoCorrelator extends GenericFunction
+public class PhotoCorrelator extends Correlator
 {
-       private JDialog _dialog;
-       private JButton _nextButton = null, _backButton = null;
-       private JButton _okButton = null;
-       private JPanel _cards = null;
-       private JTable _photoSelectionTable = null;
-       private JLabel _tipLabel = null;
-       private JTextField _offsetHourBox = null, _offsetMinBox = null, _offsetSecBox = null;
-       private JRadioButton _photoLaterOption = null, _pointLaterOption = null;
-       private JRadioButton _timeLimitRadio = null, _distLimitRadio = null;
-       private JTextField _limitMinBox = null, _limitSecBox = null;
-       private JTextField _limitDistBox = null;
-       private JComboBox _distUnitsDropdown = null;
-       private JTable _previewTable = null;
-       private boolean _firstTabAvailable = false;
-       private boolean _previewEnabled = false; // flag required to enable preview function on second panel
-
-
        /**
         * Constructor
         * @param inApp App object to report actions to
         */
-       public PhotoCorrelator(App inApp)
-       {
+       public PhotoCorrelator(App inApp) {
                super(inApp);
        }
 
@@ -77,360 +34,34 @@ public class PhotoCorrelator extends GenericFunction
                return "function.correlatephotos";
        }
 
-       /**
-        * Reset dialog and show it
-        */
-       public void begin()
-       {
-               // First create dialog if necessary
-               if (_dialog == null)
-               {
-                       _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
-                       _dialog.setLocationRelativeTo(_parentFrame);
-                       _dialog.getContentPane().add(makeDialogContents());
-                       _dialog.pack();
-               }
-               // 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;
-               }
-               // Check for any non-correlated photos, show warning continue/cancel
-               if (!trackHasUncorrelatedPhotos())
-               {
-                       Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")};
-                       if (JOptionPane.showOptionDialog(_parentFrame, I18nManager.getText("dialog.correlate.nouncorrelatedphotos"),
-                                       I18nManager.getText(getNameKey()), JOptionPane.YES_NO_OPTION,
-                                       JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
-                               == JOptionPane.NO_OPTION)
-                       {
-                               return;
-                       }
-               }
-               PhotoSelectionTableModel model = makePhotoSelectionTableModel(_app.getTrackInfo());
-               _firstTabAvailable = model != null && model.getRowCount() > 0;
-               CardLayout cl = (CardLayout) _cards.getLayout();
-               if (_firstTabAvailable)
-               {
-                       cl.first(_cards);
-                       _nextButton.setEnabled(true);
-                       _backButton.setEnabled(false);
-                       _tipLabel.setVisible(false);
-                       _photoSelectionTable.setModel(model);
-                       _previewEnabled = false;
-                       for (int i=0; i<model.getColumnCount(); i++) {
-                               _photoSelectionTable.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);
-                       _photoSelectionTable.getSelectionModel().setSelectionInterval(preselectedIndex, preselectedIndex);
-                       _nextButton.requestFocus();
-               }
-               else
-               {
-                       _tipLabel.setVisible(true);
-                       setupSecondCard(null);
-               }
-               _dialog.setVisible(true);
+       /** @return type key */
+       protected String getMediaTypeKey() {
+               return "photo";
        }
 
-
-       /**
-        * 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 JPanel();
-               _cards.setLayout(new CardLayout());
-
-               // First panel for photo selection table
-               JPanel card1 = new JPanel();
-               card1.setLayout(new BorderLayout(10, 10));
-               card1.add(new JLabel(I18nManager.getText("dialog.correlate.photoselect.intro")), BorderLayout.NORTH);
-               _photoSelectionTable = new JTable();
-               JScrollPane photoScrollPane = new JScrollPane(_photoSelectionTable);
-               photoScrollPane.setPreferredSize(new Dimension(400, 100));
-               card1.add(photoScrollPane, BorderLayout.CENTER);
-               _cards.add(card1, "card1");
-
-               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));
-               _tipLabel = new JLabel(I18nManager.getText("dialog.correlate.options.tip"));
-               _tipLabel.setBorder(BorderFactory.createEmptyBorder(8, 6, 5, 6));
-               card2Top.add(_tipLabel);
-               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);
-               _photoLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.photolater"));
-               _pointLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.pointlater"));
-               _photoLaterOption.addItemListener(optionsChangedListener);
-               _pointLaterOption.addItemListener(optionsChangedListener);
-               ButtonGroup laterGroup = new ButtonGroup();
-               laterGroup.add(_photoLaterOption);
-               laterGroup.add(_pointLaterOption);
-               offsetPanelBot.add(_photoLaterOption);
-               offsetPanelBot.add(_pointLaterOption);
-               offsetPanel.add(offsetPanelBot);
-               offsetPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
-               card2Top.add(offsetPanel);
-
-               // 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);
-               timeLimitPanel.add(noTimeLimitRadio);
-               _timeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.timelimit") + " : ");
-               _timeLimitRadio.addItemListener(optionsChangedListener);
-               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);
-               distLimitPanel.add(noDistLimitRadio);
-               _distLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.distancelimit"));
-               _distLimitRadio.addItemListener(optionsChangedListener);
-               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(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 PhotoPreviewTableModel());
-               JScrollPane previewScrollPane = new JScrollPane(_previewTable);
-               previewScrollPane.setPreferredSize(new Dimension(300, 100));
-               card2.add(previewScrollPane, BorderLayout.CENTER);
-               _cards.add(card2, "card2");
-               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)
-                               {
-                                       CardLayout cl = (CardLayout) _cards.getLayout();
-                                       cl.previous(_cards);
-                                       _backButton.setEnabled(false);
-                                       _nextButton.setEnabled(true);
-                                       _okButton.setEnabled(false);
-                                       _previewEnabled = false;
-                               }
-                       });
-               _backButton.setEnabled(false);
-               buttonPanel.add(_backButton);
-               _nextButton = new JButton(I18nManager.getText("button.next"));
-               _nextButton.addActionListener(new ActionListener()
-                       {
-                               public void actionPerformed(ActionEvent e)
-                               {
-                                       int rowNum = _photoSelectionTable.getSelectedRow();
-                                       if (rowNum < 0) {rowNum = 0;}
-                                       PhotoSelectionTableRow selectedRow = ((PhotoSelectionTableModel) _photoSelectionTable.getModel())
-                                               .getRow(rowNum);
-                                       setupSecondCard(selectedRow.getTimeDiff());
-                               }
-                       });
-               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 selection table
-        * @param inTrackInfo track info object
-        * @return table model
-        */
-       private static PhotoSelectionTableModel makePhotoSelectionTableModel(TrackInfo inTrackInfo)
-       {
-               PhotoSelectionTableModel model = new PhotoSelectionTableModel();
-               int numPhotos = inTrackInfo.getPhotoList().getNumPhotos();
-               for (int i=0; i<numPhotos; i++)
-               {
-                       Photo photo = inTrackInfo.getPhotoList().getPhoto(i);
-                       // For working out time differences, can't use photos which already had point information
-                       if (photo.getDataPoint() != null && photo.getDataPoint().hasTimestamp()
-                               && photo.getOriginalStatus() == Photo.Status.NOT_CONNECTED)
-                       {
-                               // Calculate time difference, add to table model
-                               long timeDiff = photo.getTimestamp().getSecondsSince(photo.getDataPoint().getTimestamp());
-                               model.addPhoto(photo, timeDiff);
-                       }
-               }
-               return model;
-       }
-
-
-       /**
-        * Group the two radio buttons together with a ButtonGroup
-        * @param inButton1 first radio button
-        * @param inButton2 second radio button
-        */
-       private static void groupRadioButtons(JRadioButton inButton1, JRadioButton inButton2)
-       {
-               ButtonGroup buttonGroup = new ButtonGroup();
-               buttonGroup.add(inButton1);
-               buttonGroup.add(inButton2);
-               inButton1.setSelected(true);
+       /** @return photo list*/
+       protected MediaList getMediaList() {
+               return _app.getTrackInfo().getPhotoList();
        }
 
-
-       /**
-        * Set up the second card using the given time difference and show it
-        * @param inTimeDiff time difference to use for photo time offsets
-        */
-       private void setupSecondCard(TimeDifference inTimeDiff)
-       {
-               _previewEnabled = false;
-               boolean hasTimeDiff = inTimeDiff != null;
-               if (!hasTimeDiff)
-               {
-                       // No time difference available, so calculate based on computer's time zone
-                       inTimeDiff = getTimezoneOffset();
-               }
-               // Use time difference to set edit boxes
-               _offsetHourBox.setText("" + inTimeDiff.getNumHours());
-               _offsetMinBox.setText("" + inTimeDiff.getNumMinutes());
-               _offsetSecBox.setText("" + inTimeDiff.getNumSeconds());
-               _photoLaterOption.setSelected(inTimeDiff.getIsPositive());
-               _pointLaterOption.setSelected(!inTimeDiff.getIsPositive());
-               createPreview(inTimeDiff, true);
-               CardLayout cl = (CardLayout) _cards.getLayout();
-               cl.last(_cards);
-               _backButton.setEnabled(hasTimeDiff);
-               _nextButton.setEnabled(false);
-               // enable ok button if any photos have been selected
-               _okButton.setEnabled(((PhotoPreviewTableModel) _previewTable.getModel()).hasPhotosSelected());
-               _previewEnabled = true;
-       }
-
-
-       /**
-        * 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 = _photoLaterOption.isSelected();
-               createPreview(new TimeDifference(numHours, numMins, numSecs, isPos), inFromButton);
-       }
-
-
        /**
         * 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
         */
-       private void createPreview(TimeDifference inTimeDiff, boolean inShowWarning)
+       protected void createPreview(TimeDifference inTimeDiff, boolean inShowWarning)
        {
                TimeDifference timeLimit = parseTimeLimit();
                double angDistLimit = parseDistanceLimit();
-               PhotoPreviewTableModel model = new PhotoPreviewTableModel();
+               MediaPreviewTableModel model = new MediaPreviewTableModel("dialog.correlate.select.photoname");
                PhotoList photos = _app.getTrackInfo().getPhotoList();
                // Loop through photos deciding whether to set correlate flag or not
                int numPhotos = photos.getNumPhotos();
                for (int i=0; i<numPhotos; i++)
                {
                        Photo photo = photos.getPhoto(i);
-                       PointPair pair = getPointPairForPhoto(_app.getTrackInfo().getTrack(), photo, inTimeDiff);
-                       PhotoPreviewTableRow row = new PhotoPreviewTableRow(pair);
+                       PointMediaPair pair = getPointPairForMedia(_app.getTrackInfo().getTrack(), photo, inTimeDiff);
+                       MediaPreviewTableRow row = new MediaPreviewTableRow(pair);
                        // Don't try to correlate photos which don't have points either side
                        boolean correlatePhoto = pair.isValid();
                        // Don't select photos which already have a point
@@ -453,7 +84,7 @@ public class PhotoCorrelator extends GenericFunction
                                correlatePhoto = false;
                        }
                        row.setCorrelateFlag(correlatePhoto);
-                       model.addPhotoRow(row);
+                       model.addRow(row);
                }
                _previewTable.setModel(model);
                // Set distance units
@@ -465,218 +96,22 @@ public class PhotoCorrelator extends GenericFunction
                        _previewTable.getColumnModel().getColumn(i).setPreferredWidth(colWidths[i]);
                }
                // check if any photos found
-               _okButton.setEnabled(model.hasPhotosSelected());
-               if (inShowWarning && !model.hasPhotosSelected())
+               _okButton.setEnabled(model.hasAnySelected());
+               if (inShowWarning && !model.hasAnySelected())
                {
                        JOptionPane.showMessageDialog(_dialog, I18nManager.getText("dialog.correlate.alloutsiderange"),
                                I18nManager.getText(getNameKey()), JOptionPane.ERROR_MESSAGE);
                }
        }
 
-       /**
-        * Parse the time limit values entered and validate them
-        * @return TimeDifference object describing limit
-        */
-       private 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
-        */
-       private 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
-        */
-       private Distance.Units getSelectedDistanceUnits()
-       {
-               final Distance.Units[] distUnits = {Distance.Units.KILOMETRES, Distance.Units.METRES, Distance.Units.MILES};
-               return distUnits[_distUnitsDropdown.getSelectedIndex()];
-       }
-
-
-       /**
-        * Try to parse the given string
-        * @param inText String to parse
-        * @return value if parseable, 0 otherwise
-        */
-       private static int getValue(String inText)
-       {
-               int value = 0;
-               try {
-                       value = Integer.parseInt(inText);
-               }
-               catch (NumberFormatException nfe) {}
-               return value;
-       }
-
-
-       /**
-        * Get the point pair surrounding the given photo
-        * @param inTrack track object
-        * @param inPhoto photo object
-        * @param inOffset time offset to apply to photos
-        * @return point pair resulting from correlation
-        */
-       private static PointPair getPointPairForPhoto(Track inTrack, Photo inPhoto, TimeDifference inOffset)
-       {
-               PointPair pair = new PointPair(inPhoto);
-               // Add/subtract offet to photo timestamp
-               Timestamp photoStamp = inPhoto.getTimestamp().createMinusOffset(inOffset);
-               int numPoints = inTrack.getNumPoints();
-               for (int i=0; i<numPoints; i++)
-               {
-                       DataPoint point = inTrack.getPoint(i);
-                       if (point.getPhoto() == null || point.getPhoto().getCurrentStatus() != Photo.Status.TAGGED)
-                       {
-                               Timestamp pointStamp = point.getTimestamp();
-                               if (pointStamp != null && pointStamp.isValid())
-                               {
-                                       long numSeconds = pointStamp.getSecondsSince(photoStamp);
-                                       pair.addPoint(point, numSeconds);
-                               }
-                       }
-               }
-               return pair;
-       }
-
-
-       /**
-        * Construct an array of the point pairs to use for correlation
-        * @return array of PointPair objects
-        */
-       private PointPair[] getPointPairs()
-       {
-               PhotoPreviewTableModel model = (PhotoPreviewTableModel) _previewTable.getModel();
-               int numPhotos = model.getRowCount();
-               PointPair[] pairs = new PointPair[numPhotos];
-               // Loop over photos in preview table model
-               for (int i=0; i<numPhotos; i++)
-               {
-                       PhotoPreviewTableRow row = model.getRow(i);
-                       // add all selected pairs to array (other elements remain null)
-                       if (row.getCorrelateFlag().booleanValue())
-                       {
-                               pairs[i] = row.getPointPair();
-                       }
-               }
-               return pairs;
-       }
-
-       /**
-        * @return time difference of local time zone from UTC when the first photo was taken
-        */
-       private TimeDifference getTimezoneOffset()
-       {
-               Calendar cal = null;
-               // Base time difference on DST when first photo was taken
-               Photo firstPhoto = _app.getTrackInfo().getPhotoList().getPhoto(0);
-               if (firstPhoto != null && firstPhoto.getTimestamp() != null) {
-                       cal = firstPhoto.getTimestamp().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
-        */
-       private static int getMedianIndex(PhotoSelectionTableModel 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++)
-               {
-                       PhotoSelectionTableRow 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);
-               }
-       }
-
-
-       /**
-        * Check if the track has any uncorrelated photos
-        * @return true if there are any photos which are not connected to points
-        */
-       private boolean trackHasUncorrelatedPhotos()
-       {
-               PhotoList photoList = _app.getTrackInfo().getPhotoList();
-               int numPhotos = photoList.getNumPhotos();
-               // loop over photos
-               for (int i=0; i<numPhotos; i++)
-               {
-                       Photo photo = photoList.getPhoto(i);
-                       if (photo != null && photo.getDataPoint() == null) {
-                               return true;
-                       }
-               }
-               // no uncorrelated photos found
-               return false;
-       }
 
        /**
         * Finish the correlation by modifying the track
         * and passing the Undo information back to the App
         */
-       private void finishCorrelation()
+       protected void finishCorrelation()
        {
-               PointPair[] pointPairs = getPointPairs();
+               PointMediaPair[] pointPairs = getPointPairs();
                if (pointPairs == null || pointPairs.length <= 0) {return;}
 
                // begin to construct undo information
@@ -685,7 +120,7 @@ public class PhotoCorrelator extends GenericFunction
                int arraySize = pointPairs.length;
                int i = 0, numPhotos = 0;
                int numPointsToCreate = 0;
-               PointPair pair = null;
+               PointMediaPair pair = null;
                for (i=0; i<arraySize; i++)
                {
                        pair = pointPairs[i];
@@ -698,10 +133,10 @@ public class PhotoCorrelator extends GenericFunction
                                        if (pointPhoto == null)
                                        {
                                                // photo coincides with photoless point so connect the two
-                                               pair.getPointBefore().setPhoto(pair.getPhoto());
-                                               pair.getPhoto().setDataPoint(pair.getPointBefore());
+                                               pair.getPointBefore().setPhoto((Photo) pair.getMedia());
+                                               pair.getMedia().setDataPoint(pair.getPointBefore());
                                        }
-                                       else if (pointPhoto.equals(pair.getPhoto())) {
+                                       else if (pointPhoto.equals(pair.getMedia())) {
                                                // photo is already connected, nothing to do
                                        }
                                        else {
@@ -731,7 +166,7 @@ public class PhotoCorrelator extends GenericFunction
                                {
                                        pointToAdd = null;
                                        if (pair.getMinSeconds() == 0L && pair.getPointBefore().getPhoto() != null
-                                        && !pair.getPointBefore().getPhoto().equals(pair.getPhoto()))
+                                        && !pair.getPointBefore().getPhoto().equals(pair.getMedia()))
                                        {
                                                // clone point
                                                pointToAdd = pair.getPointBefore().clonePoint();
@@ -744,8 +179,8 @@ public class PhotoCorrelator extends GenericFunction
                                        if (pointToAdd != null)
                                        {
                                                // link photo to point
-                                               pointToAdd.setPhoto(pair.getPhoto());
-                                               pair.getPhoto().setDataPoint(pointToAdd);
+                                               pointToAdd.setPhoto((Photo) pair.getMedia());
+                                               pair.getMedia().setDataPoint(pointToAdd);
                                                // set to start of segment so not joined in track
                                                pointToAdd.setSegmentStart(true);
                                                // add to point array
@@ -761,7 +196,10 @@ public class PhotoCorrelator extends GenericFunction
                // send undo information back to controlling app
                undo.setNumPhotosCorrelated(numPhotos);
                _app.completeFunction(undo, ("" + numPhotos + " "
-                        + (numPhotos==1?I18nManager.getText("confirm.correlate.single"):I18nManager.getText("confirm.correlate.multi"))));
-               // observers already informed by track update
+                        + (numPhotos==1?I18nManager.getText("confirm.correlatephotos.single"):I18nManager.getText("confirm.correlatephotos.multi"))));
+               // observers already informed by track update if new points created
+               if (numPointsToCreate == 0) {
+                       UpdateMessageBroker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
+               }
        }
 }