1 package tim.prune.correlate;
3 import java.awt.BorderLayout;
4 import java.awt.CardLayout;
5 import java.awt.Component;
6 import java.awt.Dimension;
7 import java.awt.FlowLayout;
8 import java.awt.event.ActionEvent;
9 import java.awt.event.ActionListener;
10 import java.util.Calendar;
11 import java.util.Iterator;
12 import java.util.TreeSet;
14 import javax.swing.BorderFactory;
15 import javax.swing.BoxLayout;
16 import javax.swing.ButtonGroup;
17 import javax.swing.JButton;
18 import javax.swing.JComboBox;
19 import javax.swing.JDialog;
20 import javax.swing.JLabel;
21 import javax.swing.JOptionPane;
22 import javax.swing.JPanel;
23 import javax.swing.JRadioButton;
24 import javax.swing.JScrollPane;
25 import javax.swing.JTable;
26 import javax.swing.JTextField;
29 import tim.prune.GenericFunction;
30 import tim.prune.I18nManager;
31 import tim.prune.data.DataPoint;
32 import tim.prune.data.Distance;
33 import tim.prune.data.Field;
34 import tim.prune.data.Photo;
35 import tim.prune.data.PhotoList;
36 import tim.prune.data.TimeDifference;
37 import tim.prune.data.Timestamp;
38 import tim.prune.data.Track;
39 import tim.prune.data.TrackInfo;
40 import tim.prune.undo.UndoCorrelatePhotos;
43 * Class to manage the automatic correlation of photos to points
44 * including the GUI stuff to control the correlation options
46 public class PhotoCorrelator extends GenericFunction
48 private JDialog _dialog;
49 private JButton _nextButton = null, _backButton = null;
50 private JButton _okButton = null;
51 private JPanel _cards = null;
52 private JTable _photoSelectionTable = null;
53 private JLabel _tipLabel = null;
54 private JTextField _offsetHourBox = null, _offsetMinBox = null, _offsetSecBox = null;
55 private JRadioButton _photoLaterOption = null, _pointLaterOption = null;
56 private JRadioButton _timeLimitRadio = null, _distLimitRadio = null;
57 private JTextField _limitMinBox = null, _limitSecBox = null;
58 private JTextField _limitDistBox = null;
59 private JComboBox _distUnitsDropdown = null;
60 private JTable _previewTable = null;
61 private boolean _firstTabAvailable = false;
62 private boolean _previewEnabled = false; // flag required to enable preview function on second panel
67 * @param inApp App object to report actions to
69 public PhotoCorrelator(App inApp)
75 /** Get the name key */
76 public String getNameKey() {
77 return "function.correlatephotos";
81 * Reset dialog and show it
85 // First create dialog if necessary
88 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
89 _dialog.setLocationRelativeTo(_parentFrame);
90 _dialog.getContentPane().add(makeDialogContents());
93 // Check whether track has timestamps, exit if not
94 if (!_app.getTrackInfo().getTrack().hasData(Field.TIMESTAMP))
96 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.correlate.notimestamps"),
97 I18nManager.getText(getNameKey()), JOptionPane.INFORMATION_MESSAGE);
100 // Check for any non-correlated photos, show warning continue/cancel
101 if (!trackHasUncorrelatedPhotos())
103 Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")};
104 if (JOptionPane.showOptionDialog(_parentFrame, I18nManager.getText("dialog.correlate.nouncorrelatedphotos"),
105 I18nManager.getText(getNameKey()), JOptionPane.YES_NO_OPTION,
106 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
107 == JOptionPane.NO_OPTION)
112 PhotoSelectionTableModel model = makePhotoSelectionTableModel(_app.getTrackInfo());
113 _firstTabAvailable = model != null && model.getRowCount() > 0;
114 CardLayout cl = (CardLayout) _cards.getLayout();
115 if (_firstTabAvailable)
118 _nextButton.setEnabled(true);
119 _backButton.setEnabled(false);
120 _tipLabel.setVisible(false);
121 _photoSelectionTable.setModel(model);
122 _previewEnabled = false;
123 for (int i=0; i<model.getColumnCount(); i++) {
124 _photoSelectionTable.getColumnModel().getColumn(i).setPreferredWidth(i==3?50:150);
126 // Calculate median time difference, select corresponding row of table
127 int preselectedIndex = model.getRowCount() < 3 ? 0 : getMedianIndex(model);
128 _photoSelectionTable.getSelectionModel().setSelectionInterval(preselectedIndex, preselectedIndex);
129 _nextButton.requestFocus();
133 _tipLabel.setVisible(true);
134 setupSecondCard(null);
136 _dialog.setVisible(true);
141 * Make contents of correlate dialog
142 * @return JPanel containing gui elements
144 private JPanel makeDialogContents()
146 JPanel mainPanel = new JPanel();
147 mainPanel.setLayout(new BorderLayout());
148 // Card panel in the middle
149 _cards = new JPanel();
150 _cards.setLayout(new CardLayout());
152 // First panel for photo selection table
153 JPanel card1 = new JPanel();
154 card1.setLayout(new BorderLayout(10, 10));
155 card1.add(new JLabel(I18nManager.getText("dialog.correlate.photoselect.intro")), BorderLayout.NORTH);
156 _photoSelectionTable = new JTable();
157 JScrollPane photoScrollPane = new JScrollPane(_photoSelectionTable);
158 photoScrollPane.setPreferredSize(new Dimension(400, 100));
159 card1.add(photoScrollPane, BorderLayout.CENTER);
160 _cards.add(card1, "card1");
162 OptionsChangedListener optionsChangedListener = new OptionsChangedListener(this);
163 // Second panel for options
164 JPanel card2 = new JPanel();
165 card2.setLayout(new BorderLayout());
166 JPanel card2Top = new JPanel();
167 card2Top.setLayout(new BoxLayout(card2Top, BoxLayout.Y_AXIS));
168 _tipLabel = new JLabel(I18nManager.getText("dialog.correlate.options.tip"));
169 card2Top.add(_tipLabel);
170 card2Top.add(new JLabel(I18nManager.getText("dialog.correlate.options.intro")));
171 // time offset section
172 JPanel offsetPanel = new JPanel();
173 offsetPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.offsetpanel")));
174 offsetPanel.setLayout(new BoxLayout(offsetPanel, BoxLayout.Y_AXIS));
175 JPanel offsetPanelTop = new JPanel();
176 offsetPanelTop.setLayout(new FlowLayout());
177 offsetPanelTop.setBorder(null);
178 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset") + ": "));
179 _offsetHourBox = new JTextField(3);
180 _offsetHourBox.addKeyListener(optionsChangedListener);
181 offsetPanelTop.add(_offsetHourBox);
182 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.hours")));
183 _offsetMinBox = new JTextField(3);
184 _offsetMinBox.addKeyListener(optionsChangedListener);
185 offsetPanelTop.add(_offsetMinBox);
186 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
187 _offsetSecBox = new JTextField(3);
188 _offsetSecBox.addKeyListener(optionsChangedListener);
189 offsetPanelTop.add(_offsetSecBox);
190 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
191 offsetPanel.add(offsetPanelTop);
193 // radio buttons for photo / point later
194 JPanel offsetPanelBot = new JPanel();
195 offsetPanelBot.setLayout(new FlowLayout());
196 offsetPanelBot.setBorder(null);
197 _photoLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.photolater"));
198 _pointLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.pointlater"));
199 _photoLaterOption.addItemListener(optionsChangedListener);
200 _pointLaterOption.addItemListener(optionsChangedListener);
201 ButtonGroup laterGroup = new ButtonGroup();
202 laterGroup.add(_photoLaterOption);
203 laterGroup.add(_pointLaterOption);
204 offsetPanelBot.add(_photoLaterOption);
205 offsetPanelBot.add(_pointLaterOption);
206 offsetPanel.add(offsetPanelBot);
207 offsetPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
208 card2Top.add(offsetPanel);
210 // time limits section
211 JPanel limitsPanel = new JPanel();
212 limitsPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.limitspanel")));
213 limitsPanel.setLayout(new BoxLayout(limitsPanel, BoxLayout.Y_AXIS));
214 JPanel timeLimitPanel = new JPanel();
215 timeLimitPanel.setLayout(new FlowLayout());
216 JRadioButton noTimeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.notimelimit"));
217 noTimeLimitRadio.addItemListener(optionsChangedListener);
218 timeLimitPanel.add(noTimeLimitRadio);
219 _timeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.timelimit") + " : ");
220 _timeLimitRadio.addItemListener(optionsChangedListener);
221 timeLimitPanel.add(_timeLimitRadio);
222 groupRadioButtons(noTimeLimitRadio, _timeLimitRadio);
223 _limitMinBox = new JTextField(3);
224 _limitMinBox.addKeyListener(optionsChangedListener);
225 timeLimitPanel.add(_limitMinBox);
226 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
227 _limitSecBox = new JTextField(3);
228 _limitSecBox.addKeyListener(optionsChangedListener);
229 timeLimitPanel.add(_limitSecBox);
230 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
231 limitsPanel.add(timeLimitPanel);
233 JPanel distLimitPanel = new JPanel();
234 distLimitPanel.setLayout(new FlowLayout());
235 JRadioButton noDistLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.nodistancelimit"));
236 noDistLimitRadio.addItemListener(optionsChangedListener);
237 distLimitPanel.add(noDistLimitRadio);
238 _distLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.distancelimit"));
239 _distLimitRadio.addItemListener(optionsChangedListener);
240 distLimitPanel.add(_distLimitRadio);
241 groupRadioButtons(noDistLimitRadio, _distLimitRadio);
242 _limitDistBox = new JTextField(4);
243 _limitDistBox.addKeyListener(optionsChangedListener);
244 distLimitPanel.add(_limitDistBox);
245 String[] distUnitsOptions = {I18nManager.getText("units.kilometres"), I18nManager.getText("units.metres"),
246 I18nManager.getText("units.miles")};
247 _distUnitsDropdown = new JComboBox(distUnitsOptions);
248 _distUnitsDropdown.addItemListener(optionsChangedListener);
249 distLimitPanel.add(_distUnitsDropdown);
250 limitsPanel.add(distLimitPanel);
251 limitsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
252 card2Top.add(limitsPanel);
255 JButton previewButton = new JButton(I18nManager.getText("button.preview"));
256 previewButton.addActionListener(new ActionListener() {
257 public void actionPerformed(ActionEvent e)
262 card2Top.add(previewButton);
263 card2.add(card2Top, BorderLayout.NORTH);
265 _previewTable = new JTable();
266 JScrollPane previewScrollPane = new JScrollPane(_previewTable);
267 previewScrollPane.setPreferredSize(new Dimension(300, 100));
268 card2.add(previewScrollPane, BorderLayout.CENTER);
269 _cards.add(card2, "card2");
270 mainPanel.add(_cards, BorderLayout.CENTER);
272 // Button panel at the bottom
273 JPanel buttonPanel = new JPanel();
274 _backButton = new JButton(I18nManager.getText("button.back"));
275 _backButton.addActionListener(new ActionListener()
277 public void actionPerformed(ActionEvent e)
279 CardLayout cl = (CardLayout) _cards.getLayout();
281 _backButton.setEnabled(false);
282 _nextButton.setEnabled(true);
283 _okButton.setEnabled(false);
284 _previewEnabled = false;
287 _backButton.setEnabled(false);
288 buttonPanel.add(_backButton);
289 _nextButton = new JButton(I18nManager.getText("button.next"));
290 _nextButton.addActionListener(new ActionListener()
292 public void actionPerformed(ActionEvent e)
294 int rowNum = _photoSelectionTable.getSelectedRow();
295 if (rowNum < 0) {rowNum = 0;}
296 PhotoSelectionTableRow selectedRow = ((PhotoSelectionTableModel) _photoSelectionTable.getModel())
298 setupSecondCard(selectedRow.getTimeDiff());
301 buttonPanel.add(_nextButton);
302 _okButton = new JButton(I18nManager.getText("button.ok"));
303 _okButton.addActionListener(new ActionListener()
305 public void actionPerformed(ActionEvent e)
311 _okButton.setEnabled(false);
312 buttonPanel.add(_okButton);
313 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
314 cancelButton.addActionListener(new ActionListener()
316 public void actionPerformed(ActionEvent e)
321 buttonPanel.add(cancelButton);
322 mainPanel.add(buttonPanel, BorderLayout.SOUTH);
328 * Construct a table model for the photo selection table
329 * @param inTrackInfo track info object
330 * @return table model
332 private static PhotoSelectionTableModel makePhotoSelectionTableModel(TrackInfo inTrackInfo)
334 PhotoSelectionTableModel model = new PhotoSelectionTableModel();
335 int numPhotos = inTrackInfo.getPhotoList().getNumPhotos();
336 for (int i=0; i<numPhotos; i++)
338 Photo photo = inTrackInfo.getPhotoList().getPhoto(i);
339 if (photo.getDataPoint() != null && photo.getDataPoint().hasTimestamp())
341 // Calculate time difference, add to table model
342 long timeDiff = photo.getTimestamp().getSecondsSince(photo.getDataPoint().getTimestamp());
343 model.addPhoto(photo, timeDiff);
351 * Group the two radio buttons together with a ButtonGroup
352 * @param inButton1 first radio button
353 * @param inButton2 second radio button
355 private static void groupRadioButtons(JRadioButton inButton1, JRadioButton inButton2)
357 ButtonGroup buttonGroup = new ButtonGroup();
358 buttonGroup.add(inButton1);
359 buttonGroup.add(inButton2);
360 inButton1.setSelected(true);
365 * Set up the second card using the given time difference and show it
366 * @param inTimeDiff time difference to use for photo time offsets
368 private void setupSecondCard(TimeDifference inTimeDiff)
370 _previewEnabled = false;
371 boolean hasTimeDiff = inTimeDiff != null;
374 // No time difference available, so calculate based on computer's time zone
375 inTimeDiff = getTimezoneOffset();
377 // Use time difference to set edit boxes
378 _offsetHourBox.setText("" + inTimeDiff.getNumHours());
379 _offsetMinBox.setText("" + inTimeDiff.getNumMinutes());
380 _offsetSecBox.setText("" + inTimeDiff.getNumSeconds());
381 _photoLaterOption.setSelected(inTimeDiff.getIsPositive());
382 _pointLaterOption.setSelected(!inTimeDiff.getIsPositive());
383 createPreview(inTimeDiff, true);
384 CardLayout cl = (CardLayout) _cards.getLayout();
386 _backButton.setEnabled(hasTimeDiff);
387 _nextButton.setEnabled(false);
388 // enable ok button if any photos have been selected
389 _okButton.setEnabled(((PhotoPreviewTableModel) _previewTable.getModel()).hasPhotosSelected());
390 _previewEnabled = true;
395 * Create a preview of the correlate action using the selected time difference
396 * @param inFromButton true if triggered from button press, false if automatic
398 public void createPreview(boolean inFromButton)
400 // Exit if still on first panel
401 if (!_previewEnabled) {return;}
402 // Create a TimeDifference based on the edit boxes
403 int numHours = getValue(_offsetHourBox.getText());
404 int numMins = getValue(_offsetMinBox.getText());
405 int numSecs = getValue(_offsetSecBox.getText());
406 boolean isPos = _photoLaterOption.isSelected();
407 createPreview(new TimeDifference(numHours, numMins, numSecs, isPos), inFromButton);
412 * Create a preview of the correlate action using the selected time difference
413 * @param inTimeDiff TimeDifference to use for preview
414 * @param inShowWarning true to show warning if all points out of range
416 private void createPreview(TimeDifference inTimeDiff, boolean inShowWarning)
418 TimeDifference timeLimit = parseTimeLimit();
419 double angDistLimit = parseDistanceLimit();
420 PhotoPreviewTableModel model = new PhotoPreviewTableModel();
421 PhotoList photos = _app.getTrackInfo().getPhotoList();
422 // Loop through photos deciding whether to set correlate flag or not
423 int numPhotos = photos.getNumPhotos();
424 for (int i=0; i<numPhotos; i++)
426 Photo photo = photos.getPhoto(i);
427 PointPair pair = getPointPairForPhoto(_app.getTrackInfo().getTrack(), photo, inTimeDiff);
428 PhotoPreviewTableRow row = new PhotoPreviewTableRow(pair);
429 // Don't try to correlate photos which don't have points either side
430 boolean correlatePhoto = pair.isValid();
431 // Check time limits, distance limits
432 if (timeLimit != null && correlatePhoto) {
433 long numSecs = pair.getMinSeconds();
434 correlatePhoto = (numSecs <= timeLimit.getTotalSeconds());
436 if (angDistLimit > 0.0 && correlatePhoto)
438 final double angDistPair = DataPoint.calculateRadiansBetween(pair.getPointBefore(), pair.getPointAfter());
439 double frac = pair.getFraction();
440 if (frac > 0.5) {frac = 1 - frac;}
441 final double angDistPhoto = angDistPair * frac;
442 correlatePhoto = (angDistPhoto < angDistLimit);
444 // Don't select photos which are already correlated to the same point
445 if (pair.getSecondsBefore() == 0L && pair.getPointBefore().getPhoto() != null
446 && pair.getPointBefore().getPhoto().equals(photo)) {
447 correlatePhoto = false;
449 row.setCorrelateFlag(correlatePhoto);
450 model.addPhotoRow(row);
452 _previewTable.setModel(model);
453 // Set distance units
454 model.setDistanceUnits(getSelectedDistanceUnits());
456 _previewTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
457 final int[] colWidths = {150, 160, 100, 100, 50};
458 for (int i=0; i<model.getColumnCount(); i++) {
459 _previewTable.getColumnModel().getColumn(i).setPreferredWidth(colWidths[i]);
461 // check if any photos found
462 _okButton.setEnabled(model.hasPhotosSelected());
463 if (inShowWarning && !model.hasPhotosSelected())
465 JOptionPane.showMessageDialog(_dialog, I18nManager.getText("dialog.correlate.alloutsiderange"),
466 I18nManager.getText(getNameKey()), JOptionPane.ERROR_MESSAGE);
471 * Parse the time limit values entered and validate them
472 * @return TimeDifference object describing limit
474 private TimeDifference parseTimeLimit()
476 if (!_timeLimitRadio.isSelected()) {return null;}
477 int mins = getValue(_limitMinBox.getText());
478 _limitMinBox.setText("" + mins);
479 int secs = getValue(_limitSecBox.getText());
480 _limitSecBox.setText("" + secs);
481 if (mins <= 0 && secs <= 0) {return null;}
482 return new TimeDifference(0, mins, secs, true);
486 * Parse the distance limit value entered and validate
487 * @return angular distance in radians
489 private double parseDistanceLimit()
492 if (_distLimitRadio.isSelected())
496 value = Double.parseDouble(_limitDistBox.getText());
498 catch (NumberFormatException nfe) {}
501 _limitDistBox.setText("0");
504 _limitDistBox.setText("" + value);
505 return Distance.convertDistanceToRadians(value, getSelectedDistanceUnits());
510 * @return the selected distance units from the dropdown
512 private Distance.Units getSelectedDistanceUnits()
514 final Distance.Units[] distUnits = {Distance.Units.KILOMETRES, Distance.Units.METRES, Distance.Units.MILES};
515 return distUnits[_distUnitsDropdown.getSelectedIndex()];
520 * Try to parse the given string
521 * @param inText String to parse
522 * @return value if parseable, 0 otherwise
524 private static int getValue(String inText)
528 value = Integer.parseInt(inText);
530 catch (NumberFormatException nfe) {}
536 * Get the point pair surrounding the given photo
537 * @param inTrack track object
538 * @param inPhoto photo object
539 * @param inOffset time offset to apply to photos
540 * @return point pair resulting from correlation
542 private static PointPair getPointPairForPhoto(Track inTrack, Photo inPhoto, TimeDifference inOffset)
544 PointPair pair = new PointPair(inPhoto);
545 // Add/subtract offet to photo timestamp
546 Timestamp photoStamp = inPhoto.getTimestamp().createMinusOffset(inOffset);
547 int numPoints = inTrack.getNumPoints();
548 for (int i=0; i<numPoints; i++)
550 DataPoint point = inTrack.getPoint(i);
551 Timestamp pointStamp = point.getTimestamp();
552 if (pointStamp != null && pointStamp.isValid())
554 long numSeconds = pointStamp.getSecondsSince(photoStamp);
555 pair.addPoint(point, numSeconds);
563 * Construct an array of the point pairs to use for correlation
564 * @return array of PointPair objects
566 private PointPair[] getPointPairs()
568 PhotoPreviewTableModel model = (PhotoPreviewTableModel) _previewTable.getModel();
569 int numPhotos = model.getRowCount();
570 PointPair[] pairs = new PointPair[numPhotos];
571 // Loop over photos in preview table model
572 for (int i=0; i<numPhotos; i++)
574 PhotoPreviewTableRow row = model.getRow(i);
575 // add all selected pairs to array (other elements remain null)
576 if (row.getCorrelateFlag().booleanValue())
578 pairs[i] = row.getPointPair();
585 * @return time difference of local time zone from UTC when the first photo was taken
587 private TimeDifference getTimezoneOffset()
590 // Base time difference on DST when first photo was taken
591 Photo firstPhoto = _app.getTrackInfo().getPhotoList().getPhoto(0);
592 if (firstPhoto != null && firstPhoto.getTimestamp() != null) {
593 cal = firstPhoto.getTimestamp().getCalendar();
596 // No photo or no timestamp, just use current time
597 cal = Calendar.getInstance();
599 // Both time zone offset and dst offset are based on milliseconds, so convert to seconds
600 TimeDifference timeDiff = new TimeDifference((cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 1000);
606 * Calculate the median index to select from the table
607 * @param inModel table model
608 * @return index of entry to select from table
610 private static int getMedianIndex(PhotoSelectionTableModel inModel)
612 // make sortable list
613 TreeSet<TimeIndexPair> set = new TreeSet<TimeIndexPair>();
614 // loop through rows of table adding to list
615 int numRows = inModel.getRowCount();
617 for (i=0; i<numRows; i++)
619 PhotoSelectionTableRow row = inModel.getRow(i);
620 set.add(new TimeIndexPair(row.getTimeDiff().getTotalSeconds(), i));
622 // pull out middle entry and return index
623 TimeIndexPair pair = null;
624 Iterator<TimeIndexPair> iterator = set.iterator();
625 for (i=0; i<(numRows+1)/2; i++)
627 pair = iterator.next();
629 return pair.getIndex();
634 * Disable the ok button
636 public void disableOkButton()
638 if (_okButton != null) {
639 _okButton.setEnabled(false);
645 * Check if the track has any uncorrelated photos
646 * @return true if there are any photos which are not connected to points
648 private boolean trackHasUncorrelatedPhotos()
650 PhotoList photoList = _app.getTrackInfo().getPhotoList();
651 int numPhotos = photoList.getNumPhotos();
653 for (int i=0; i<numPhotos; i++)
655 Photo photo = photoList.getPhoto(i);
656 if (photo != null && photo.getDataPoint() == null) {
660 // no uncorrelated photos found
665 * Finish the correlation by modifying the track
666 * and passing the Undo information back to the App
668 private void finishCorrelation()
670 PointPair[] pointPairs = getPointPairs();
671 if (pointPairs == null || pointPairs.length <= 0) {return;}
673 // begin to construct undo information
674 UndoCorrelatePhotos undo = new UndoCorrelatePhotos(_app.getTrackInfo());
676 int arraySize = pointPairs.length;
677 int i = 0, numPhotos = 0;
678 int numPointsToCreate = 0;
679 PointPair pair = null;
680 for (i=0; i<arraySize; i++)
682 pair = pointPairs[i];
683 if (pair != null && pair.isValid())
685 if (pair.getMinSeconds() == 0L)
688 Photo pointPhoto = pair.getPointBefore().getPhoto();
689 if (pointPhoto == null)
691 // photo coincides with photoless point so connect the two
692 pair.getPointBefore().setPhoto(pair.getPhoto());
693 pair.getPhoto().setDataPoint(pair.getPointBefore());
695 else if (pointPhoto.equals(pair.getPhoto()))
697 // photo is already connected, nothing to do
701 // point is already connected to a different photo, so need to clone point
707 // photo time falls between two points, so need to interpolate new one
713 // Second loop, to create points if necessary
714 if (numPointsToCreate > 0)
716 // make new array for added points
717 DataPoint[] addedPoints = new DataPoint[numPointsToCreate];
719 DataPoint pointToAdd = null;
720 for (i=0; i<arraySize; i++)
722 pair = pointPairs[i];
723 if (pair != null && pair.isValid())
726 if (pair.getMinSeconds() == 0L && pair.getPointBefore().getPhoto() != null
727 && !pair.getPointBefore().getPhoto().equals(pair.getPhoto()))
730 pointToAdd = pair.getPointBefore().clonePoint();
732 else if (pair.getMinSeconds() > 0L)
735 pointToAdd = DataPoint.interpolate(pair.getPointBefore(), pair.getPointAfter(), pair.getFraction());
737 if (pointToAdd != null)
739 // link photo to point
740 pointToAdd.setPhoto(pair.getPhoto());
741 pair.getPhoto().setDataPoint(pointToAdd);
742 // set to start of segment so not joined in track
743 pointToAdd.setSegmentStart(true);
744 // add to point array
745 addedPoints[pointNum] = pointToAdd;
751 _app.getTrackInfo().getTrack().appendPoints(addedPoints);
754 // send undo information back to controlling app
755 undo.setNumPhotosCorrelated(numPhotos);
756 _app.completeFunction(undo, ("" + numPhotos + " "
757 + (numPhotos==1?I18nManager.getText("confirm.correlate.single"):I18nManager.getText("confirm.correlate.multi"))));
758 // observers already informed by track update