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)
72 _dialog = new JDialog(inApp.getFrame(), I18nManager.getText(getNameKey()), true);
73 _dialog.setLocationRelativeTo(inApp.getFrame());
74 _dialog.getContentPane().add(makeDialogContents());
79 /** Get the name key */
80 public String getNameKey() {
81 return "function.correlatephotos";
85 * Reset dialog and show it
89 // Check whether track has timestamps, exit if not
90 if (!_app.getTrackInfo().getTrack().hasData(Field.TIMESTAMP))
92 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.correlate.notimestamps"),
93 I18nManager.getText(getNameKey()), JOptionPane.INFORMATION_MESSAGE);
96 // Check for any non-correlated photos, show warning continue/cancel
97 if (!trackHasUncorrelatedPhotos())
99 Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")};
100 if (JOptionPane.showOptionDialog(_parentFrame, I18nManager.getText("dialog.correlate.nouncorrelatedphotos"),
101 I18nManager.getText(getNameKey()), JOptionPane.YES_NO_OPTION,
102 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
103 == JOptionPane.NO_OPTION)
108 PhotoSelectionTableModel model = makePhotoSelectionTableModel(_app.getTrackInfo());
109 _firstTabAvailable = model != null && model.getRowCount() > 0;
110 CardLayout cl = (CardLayout) _cards.getLayout();
111 if (_firstTabAvailable)
114 _nextButton.setEnabled(true);
115 _backButton.setEnabled(false);
116 _tipLabel.setVisible(false);
117 _photoSelectionTable.setModel(model);
118 _previewEnabled = false;
119 for (int i=0; i<model.getColumnCount(); i++) {
120 _photoSelectionTable.getColumnModel().getColumn(i).setPreferredWidth(i==3?50:150);
122 // Calculate median time difference, select corresponding row of table
123 int preselectedIndex = model.getRowCount() < 3 ? 0 : getMedianIndex(model);
124 _photoSelectionTable.getSelectionModel().setSelectionInterval(preselectedIndex, preselectedIndex);
125 _nextButton.requestFocus();
129 _tipLabel.setVisible(true);
130 setupSecondCard(null);
132 _dialog.setVisible(true);
137 * Make contents of correlate dialog
138 * @return JPanel containing gui elements
140 private JPanel makeDialogContents()
142 JPanel mainPanel = new JPanel();
143 mainPanel.setLayout(new BorderLayout());
144 // Card panel in the middle
145 _cards = new JPanel();
146 _cards.setLayout(new CardLayout());
148 // First panel for photo selection table
149 JPanel card1 = new JPanel();
150 card1.setLayout(new BorderLayout(10, 10));
151 card1.add(new JLabel(I18nManager.getText("dialog.correlate.photoselect.intro")), BorderLayout.NORTH);
152 _photoSelectionTable = new JTable();
153 JScrollPane photoScrollPane = new JScrollPane(_photoSelectionTable);
154 photoScrollPane.setPreferredSize(new Dimension(400, 100));
155 card1.add(photoScrollPane, BorderLayout.CENTER);
156 _cards.add(card1, "card1");
158 OptionsChangedListener optionsChangedListener = new OptionsChangedListener(this);
159 // Second panel for options
160 JPanel card2 = new JPanel();
161 card2.setLayout(new BorderLayout());
162 JPanel card2Top = new JPanel();
163 card2Top.setLayout(new BoxLayout(card2Top, BoxLayout.Y_AXIS));
164 _tipLabel = new JLabel(I18nManager.getText("dialog.correlate.options.tip"));
165 card2Top.add(_tipLabel);
166 card2Top.add(new JLabel(I18nManager.getText("dialog.correlate.options.intro")));
167 // time offset section
168 JPanel offsetPanel = new JPanel();
169 offsetPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.offsetpanel")));
170 offsetPanel.setLayout(new BoxLayout(offsetPanel, BoxLayout.Y_AXIS));
171 JPanel offsetPanelTop = new JPanel();
172 offsetPanelTop.setLayout(new FlowLayout());
173 offsetPanelTop.setBorder(null);
174 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset") + ": "));
175 _offsetHourBox = new JTextField(3);
176 _offsetHourBox.addKeyListener(optionsChangedListener);
177 offsetPanelTop.add(_offsetHourBox);
178 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.hours")));
179 _offsetMinBox = new JTextField(3);
180 _offsetMinBox.addKeyListener(optionsChangedListener);
181 offsetPanelTop.add(_offsetMinBox);
182 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
183 _offsetSecBox = new JTextField(3);
184 _offsetSecBox.addKeyListener(optionsChangedListener);
185 offsetPanelTop.add(_offsetSecBox);
186 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
187 offsetPanel.add(offsetPanelTop);
189 // radio buttons for photo / point later
190 JPanel offsetPanelBot = new JPanel();
191 offsetPanelBot.setLayout(new FlowLayout());
192 offsetPanelBot.setBorder(null);
193 _photoLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.photolater"));
194 _pointLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.pointlater"));
195 _photoLaterOption.addItemListener(optionsChangedListener);
196 _pointLaterOption.addItemListener(optionsChangedListener);
197 ButtonGroup laterGroup = new ButtonGroup();
198 laterGroup.add(_photoLaterOption);
199 laterGroup.add(_pointLaterOption);
200 offsetPanelBot.add(_photoLaterOption);
201 offsetPanelBot.add(_pointLaterOption);
202 offsetPanel.add(offsetPanelBot);
203 offsetPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
204 card2Top.add(offsetPanel);
206 // time limits section
207 JPanel limitsPanel = new JPanel();
208 limitsPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.limitspanel")));
209 limitsPanel.setLayout(new BoxLayout(limitsPanel, BoxLayout.Y_AXIS));
210 JPanel timeLimitPanel = new JPanel();
211 timeLimitPanel.setLayout(new FlowLayout());
212 JRadioButton noTimeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.notimelimit"));
213 noTimeLimitRadio.addItemListener(optionsChangedListener);
214 timeLimitPanel.add(noTimeLimitRadio);
215 _timeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.timelimit") + " : ");
216 _timeLimitRadio.addItemListener(optionsChangedListener);
217 timeLimitPanel.add(_timeLimitRadio);
218 groupRadioButtons(noTimeLimitRadio, _timeLimitRadio);
219 _limitMinBox = new JTextField(3);
220 _limitMinBox.addKeyListener(optionsChangedListener);
221 timeLimitPanel.add(_limitMinBox);
222 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
223 _limitSecBox = new JTextField(3);
224 _limitSecBox.addKeyListener(optionsChangedListener);
225 timeLimitPanel.add(_limitSecBox);
226 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
227 limitsPanel.add(timeLimitPanel);
229 JPanel distLimitPanel = new JPanel();
230 distLimitPanel.setLayout(new FlowLayout());
231 JRadioButton noDistLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.nodistancelimit"));
232 noDistLimitRadio.addItemListener(optionsChangedListener);
233 distLimitPanel.add(noDistLimitRadio);
234 _distLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.distancelimit"));
235 _distLimitRadio.addItemListener(optionsChangedListener);
236 distLimitPanel.add(_distLimitRadio);
237 groupRadioButtons(noDistLimitRadio, _distLimitRadio);
238 _limitDistBox = new JTextField(4);
239 _limitDistBox.addKeyListener(optionsChangedListener);
240 distLimitPanel.add(_limitDistBox);
241 String[] distUnitsOptions = {I18nManager.getText("units.kilometres"), I18nManager.getText("units.metres"),
242 I18nManager.getText("units.miles")};
243 _distUnitsDropdown = new JComboBox(distUnitsOptions);
244 _distUnitsDropdown.addItemListener(optionsChangedListener);
245 distLimitPanel.add(_distUnitsDropdown);
246 limitsPanel.add(distLimitPanel);
247 limitsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
248 card2Top.add(limitsPanel);
251 JButton previewButton = new JButton(I18nManager.getText("button.preview"));
252 previewButton.addActionListener(new ActionListener() {
253 public void actionPerformed(ActionEvent e)
258 card2Top.add(previewButton);
259 card2.add(card2Top, BorderLayout.NORTH);
261 _previewTable = new JTable();
262 JScrollPane previewScrollPane = new JScrollPane(_previewTable);
263 previewScrollPane.setPreferredSize(new Dimension(300, 100));
264 card2.add(previewScrollPane, BorderLayout.CENTER);
265 _cards.add(card2, "card2");
266 mainPanel.add(_cards, BorderLayout.CENTER);
268 // Button panel at the bottom
269 JPanel buttonPanel = new JPanel();
270 _backButton = new JButton(I18nManager.getText("button.back"));
271 _backButton.addActionListener(new ActionListener()
273 public void actionPerformed(ActionEvent e)
275 CardLayout cl = (CardLayout) _cards.getLayout();
277 _backButton.setEnabled(false);
278 _nextButton.setEnabled(true);
279 _okButton.setEnabled(false);
280 _previewEnabled = false;
283 _backButton.setEnabled(false);
284 buttonPanel.add(_backButton);
285 _nextButton = new JButton(I18nManager.getText("button.next"));
286 _nextButton.addActionListener(new ActionListener()
288 public void actionPerformed(ActionEvent e)
290 int rowNum = _photoSelectionTable.getSelectedRow();
291 if (rowNum < 0) {rowNum = 0;}
292 PhotoSelectionTableRow selectedRow = ((PhotoSelectionTableModel) _photoSelectionTable.getModel())
294 setupSecondCard(selectedRow.getTimeDiff());
297 buttonPanel.add(_nextButton);
298 _okButton = new JButton(I18nManager.getText("button.ok"));
299 _okButton.addActionListener(new ActionListener()
301 public void actionPerformed(ActionEvent e)
307 _okButton.setEnabled(false);
308 buttonPanel.add(_okButton);
309 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
310 cancelButton.addActionListener(new ActionListener()
312 public void actionPerformed(ActionEvent e)
317 buttonPanel.add(cancelButton);
318 mainPanel.add(buttonPanel, BorderLayout.SOUTH);
324 * Construct a table model for the photo selection table
325 * @param inTrackInfo track info object
326 * @return table model
328 private static PhotoSelectionTableModel makePhotoSelectionTableModel(TrackInfo inTrackInfo)
330 PhotoSelectionTableModel model = new PhotoSelectionTableModel();
331 int numPhotos = inTrackInfo.getPhotoList().getNumPhotos();
332 for (int i=0; i<numPhotos; i++)
334 Photo photo = inTrackInfo.getPhotoList().getPhoto(i);
335 if (photo.getDataPoint() != null && photo.getDataPoint().hasTimestamp())
337 // Calculate time difference, add to table model
338 long timeDiff = photo.getTimestamp().getSecondsSince(photo.getDataPoint().getTimestamp());
339 model.addPhoto(photo, timeDiff);
347 * Group the two radio buttons together with a ButtonGroup
348 * @param inButton1 first radio button
349 * @param inButton2 second radio button
351 private static void groupRadioButtons(JRadioButton inButton1, JRadioButton inButton2)
353 ButtonGroup buttonGroup = new ButtonGroup();
354 buttonGroup.add(inButton1);
355 buttonGroup.add(inButton2);
356 inButton1.setSelected(true);
361 * Set up the second card using the given time difference and show it
362 * @param inTimeDiff time difference to use for photo time offsets
364 private void setupSecondCard(TimeDifference inTimeDiff)
366 _previewEnabled = false;
367 boolean hasTimeDiff = inTimeDiff != null;
370 // No time difference available, so calculate based on computer's time zone
371 inTimeDiff = getTimezoneOffset();
373 // Use time difference to set edit boxes
374 _offsetHourBox.setText("" + inTimeDiff.getNumHours());
375 _offsetMinBox.setText("" + inTimeDiff.getNumMinutes());
376 _offsetSecBox.setText("" + inTimeDiff.getNumSeconds());
377 _photoLaterOption.setSelected(inTimeDiff.getIsPositive());
378 _pointLaterOption.setSelected(!inTimeDiff.getIsPositive());
379 createPreview(inTimeDiff, true);
380 CardLayout cl = (CardLayout) _cards.getLayout();
382 _backButton.setEnabled(hasTimeDiff);
383 _nextButton.setEnabled(false);
384 // enable ok button if any photos have been selected
385 _okButton.setEnabled(((PhotoPreviewTableModel) _previewTable.getModel()).hasPhotosSelected());
386 _previewEnabled = true;
391 * Create a preview of the correlate action using the selected time difference
392 * @param inFromButton true if triggered from button press, false if automatic
394 public void createPreview(boolean inFromButton)
396 // Exit if still on first panel
397 if (!_previewEnabled) {return;}
398 // Create a TimeDifference based on the edit boxes
399 int numHours = getValue(_offsetHourBox.getText());
400 int numMins = getValue(_offsetMinBox.getText());
401 int numSecs = getValue(_offsetSecBox.getText());
402 boolean isPos = _photoLaterOption.isSelected();
403 createPreview(new TimeDifference(numHours, numMins, numSecs, isPos), inFromButton);
408 * Create a preview of the correlate action using the selected time difference
409 * @param inTimeDiff TimeDifference to use for preview
410 * @param inShowWarning true to show warning if all points out of range
412 private void createPreview(TimeDifference inTimeDiff, boolean inShowWarning)
414 TimeDifference timeLimit = parseTimeLimit();
415 double angDistLimit = parseDistanceLimit();
416 PhotoPreviewTableModel model = new PhotoPreviewTableModel();
417 PhotoList photos = _app.getTrackInfo().getPhotoList();
418 // Loop through photos deciding whether to set correlate flag or not
419 int numPhotos = photos.getNumPhotos();
420 for (int i=0; i<numPhotos; i++)
422 Photo photo = photos.getPhoto(i);
423 PointPair pair = getPointPairForPhoto(_app.getTrackInfo().getTrack(), photo, inTimeDiff);
424 PhotoPreviewTableRow row = new PhotoPreviewTableRow(pair);
425 // Don't try to correlate photos which don't have points either side
426 boolean correlatePhoto = pair.isValid();
427 // Check time limits, distance limits
428 if (timeLimit != null && correlatePhoto) {
429 long numSecs = pair.getMinSeconds();
430 correlatePhoto = (numSecs <= timeLimit.getTotalSeconds());
432 if (angDistLimit > 0.0 && correlatePhoto)
434 final double angDistPair = DataPoint.calculateRadiansBetween(pair.getPointBefore(), pair.getPointAfter());
435 double frac = pair.getFraction();
436 if (frac > 0.5) {frac = 1 - frac;}
437 final double angDistPhoto = angDistPair * frac;
438 correlatePhoto = (angDistPhoto < angDistLimit);
440 // Don't select photos which are already correlated to the same point
441 if (pair.getSecondsBefore() == 0L && pair.getPointBefore().getPhoto() != null
442 && pair.getPointBefore().getPhoto().equals(photo)) {
443 correlatePhoto = false;
445 row.setCorrelateFlag(correlatePhoto);
446 model.addPhotoRow(row);
448 _previewTable.setModel(model);
449 // Set distance units
450 model.setDistanceUnits(getSelectedDistanceUnits());
452 _previewTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
453 final int[] colWidths = {150, 160, 100, 100, 50};
454 for (int i=0; i<model.getColumnCount(); i++) {
455 _previewTable.getColumnModel().getColumn(i).setPreferredWidth(colWidths[i]);
457 // check if any photos found
458 _okButton.setEnabled(model.hasPhotosSelected());
459 if (inShowWarning && !model.hasPhotosSelected())
461 JOptionPane.showMessageDialog(_dialog, I18nManager.getText("dialog.correlate.alloutsiderange"),
462 I18nManager.getText(getNameKey()), JOptionPane.ERROR_MESSAGE);
467 * Parse the time limit values entered and validate them
468 * @return TimeDifference object describing limit
470 private TimeDifference parseTimeLimit()
472 if (!_timeLimitRadio.isSelected()) {return null;}
473 int mins = getValue(_limitMinBox.getText());
474 _limitMinBox.setText("" + mins);
475 int secs = getValue(_limitSecBox.getText());
476 _limitSecBox.setText("" + secs);
477 if (mins <= 0 && secs <= 0) {return null;}
478 return new TimeDifference(0, mins, secs, true);
482 * Parse the distance limit value entered and validate
483 * @return angular distance in radians
485 private double parseDistanceLimit()
488 if (_distLimitRadio.isSelected())
492 value = Double.parseDouble(_limitDistBox.getText());
494 catch (NumberFormatException nfe) {}
497 _limitDistBox.setText("0");
500 _limitDistBox.setText("" + value);
501 return Distance.convertDistanceToRadians(value, getSelectedDistanceUnits());
506 * @return the selected distance units from the dropdown
508 private Distance.Units getSelectedDistanceUnits()
510 final Distance.Units[] distUnits = {Distance.Units.KILOMETRES, Distance.Units.METRES, Distance.Units.MILES};
511 return distUnits[_distUnitsDropdown.getSelectedIndex()];
516 * Try to parse the given string
517 * @param inText String to parse
518 * @return value if parseable, 0 otherwise
520 private static int getValue(String inText)
524 value = Integer.parseInt(inText);
526 catch (NumberFormatException nfe) {}
532 * Get the point pair surrounding the given photo
533 * @param inTrack track object
534 * @param inPhoto photo object
535 * @param inOffset time offset to apply to photos
536 * @return point pair resulting from correlation
538 private static PointPair getPointPairForPhoto(Track inTrack, Photo inPhoto, TimeDifference inOffset)
540 PointPair pair = new PointPair(inPhoto);
541 // Add/subtract offet to photo timestamp
542 Timestamp photoStamp = inPhoto.getTimestamp().createMinusOffset(inOffset);
543 int numPoints = inTrack.getNumPoints();
544 for (int i=0; i<numPoints; i++)
546 DataPoint point = inTrack.getPoint(i);
547 Timestamp pointStamp = point.getTimestamp();
548 if (pointStamp != null && pointStamp.isValid())
550 long numSeconds = pointStamp.getSecondsSince(photoStamp);
551 pair.addPoint(point, numSeconds);
559 * Construct an array of the point pairs to use for correlation
560 * @return array of PointPair objects
562 private PointPair[] getPointPairs()
564 PhotoPreviewTableModel model = (PhotoPreviewTableModel) _previewTable.getModel();
565 int numPhotos = model.getRowCount();
566 PointPair[] pairs = new PointPair[numPhotos];
567 // Loop over photos in preview table model
568 for (int i=0; i<numPhotos; i++)
570 PhotoPreviewTableRow row = model.getRow(i);
571 // add all selected pairs to array (other elements remain null)
572 if (row.getCorrelateFlag().booleanValue())
574 pairs[i] = row.getPointPair();
581 * @return time difference of local time zone from UTC when the first photo was taken
583 private TimeDifference getTimezoneOffset()
586 // Base time difference on DST when first photo was taken
587 Photo firstPhoto = _app.getTrackInfo().getPhotoList().getPhoto(0);
588 if (firstPhoto != null && firstPhoto.getTimestamp() != null) {
589 cal = firstPhoto.getTimestamp().getCalendar();
592 // No photo or no timestamp, just use current time
593 cal = Calendar.getInstance();
595 // Both time zone offset and dst offset are based on milliseconds, so convert to seconds
596 TimeDifference timeDiff = new TimeDifference((cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 1000);
602 * Calculate the median index to select from the table
603 * @param inModel table model
604 * @return index of entry to select from table
606 private static int getMedianIndex(PhotoSelectionTableModel inModel)
608 // make sortable list
609 TreeSet<TimeIndexPair> set = new TreeSet<TimeIndexPair>();
610 // loop through rows of table adding to list
611 int numRows = inModel.getRowCount();
613 for (i=0; i<numRows; i++)
615 PhotoSelectionTableRow row = inModel.getRow(i);
616 set.add(new TimeIndexPair(row.getTimeDiff().getTotalSeconds(), i));
618 // pull out middle entry and return index
619 TimeIndexPair pair = null;
620 Iterator<TimeIndexPair> iterator = set.iterator();
621 for (i=0; i<(numRows+1)/2; i++)
623 pair = iterator.next();
625 return pair.getIndex();
630 * Disable the ok button
632 public void disableOkButton()
634 if (_okButton != null) {
635 _okButton.setEnabled(false);
641 * Check if the track has any uncorrelated photos
642 * @return true if there are any photos which are not connected to points
644 private boolean trackHasUncorrelatedPhotos()
646 PhotoList photoList = _app.getTrackInfo().getPhotoList();
647 int numPhotos = photoList.getNumPhotos();
649 for (int i=0; i<numPhotos; i++)
651 Photo photo = photoList.getPhoto(i);
652 if (photo != null && photo.getDataPoint() == null) {
656 // no uncorrelated photos found
661 * Finish the correlation by modifying the track
662 * and passing the Undo information back to the App
664 private void finishCorrelation()
666 PointPair[] pointPairs = getPointPairs();
667 if (pointPairs == null || pointPairs.length <= 0) {return;}
669 // begin to construct undo information
670 UndoCorrelatePhotos undo = new UndoCorrelatePhotos(_app.getTrackInfo());
672 int arraySize = pointPairs.length;
673 int i = 0, numPhotos = 0;
674 int numPointsToCreate = 0;
675 PointPair pair = null;
676 for (i=0; i<arraySize; i++)
678 pair = pointPairs[i];
679 if (pair != null && pair.isValid())
681 if (pair.getMinSeconds() == 0L)
684 Photo pointPhoto = pair.getPointBefore().getPhoto();
685 if (pointPhoto == null)
687 // photo coincides with photoless point so connect the two
688 pair.getPointBefore().setPhoto(pair.getPhoto());
689 pair.getPhoto().setDataPoint(pair.getPointBefore());
691 else if (pointPhoto.equals(pair.getPhoto()))
693 // photo is already connected, nothing to do
697 // point is already connected to a different photo, so need to clone point
703 // photo time falls between two points, so need to interpolate new one
709 // Second loop, to create points if necessary
710 if (numPointsToCreate > 0)
712 // make new array for added points
713 DataPoint[] addedPoints = new DataPoint[numPointsToCreate];
715 DataPoint pointToAdd = null;
716 for (i=0; i<arraySize; i++)
718 pair = pointPairs[i];
719 if (pair != null && pair.isValid())
722 if (pair.getMinSeconds() == 0L && pair.getPointBefore().getPhoto() != null
723 && !pair.getPointBefore().getPhoto().equals(pair.getPhoto()))
726 pointToAdd = pair.getPointBefore().clonePoint();
728 else if (pair.getMinSeconds() > 0L)
731 pointToAdd = DataPoint.interpolate(pair.getPointBefore(), pair.getPointAfter(), pair.getFraction());
733 if (pointToAdd != null)
735 // link photo to point
736 pointToAdd.setPhoto(pair.getPhoto());
737 pair.getPhoto().setDataPoint(pointToAdd);
738 // set to start of segment so not joined in track
739 pointToAdd.setSegmentStart(true);
740 // add to point array
741 addedPoints[pointNum] = pointToAdd;
747 _app.getTrackInfo().getTrack().appendPoints(addedPoints);
750 // send undo information back to controlling app
751 undo.setNumPhotosCorrelated(numPhotos);
752 _app.completeFunction(undo, ("" + numPhotos + " "
753 + (numPhotos==1?I18nManager.getText("confirm.correlate.single"):I18nManager.getText("confirm.correlate.multi"))));
754 // observers already informed by track update