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 _tipLabel.setBorder(BorderFactory.createEmptyBorder(8, 6, 5, 6));
170 card2Top.add(_tipLabel);
171 JLabel introLabel = new JLabel(I18nManager.getText("dialog.correlate.options.intro"));
172 introLabel.setBorder(BorderFactory.createEmptyBorder(8, 6, 5, 6));
173 card2Top.add(introLabel);
174 // time offset section
175 JPanel offsetPanel = new JPanel();
176 offsetPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.offsetpanel")));
177 offsetPanel.setLayout(new BoxLayout(offsetPanel, BoxLayout.Y_AXIS));
178 JPanel offsetPanelTop = new JPanel();
179 offsetPanelTop.setLayout(new FlowLayout());
180 offsetPanelTop.setBorder(null);
181 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset") + ": "));
182 _offsetHourBox = new JTextField(3);
183 _offsetHourBox.addKeyListener(optionsChangedListener);
184 offsetPanelTop.add(_offsetHourBox);
185 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.hours")));
186 _offsetMinBox = new JTextField(3);
187 _offsetMinBox.addKeyListener(optionsChangedListener);
188 offsetPanelTop.add(_offsetMinBox);
189 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
190 _offsetSecBox = new JTextField(3);
191 _offsetSecBox.addKeyListener(optionsChangedListener);
192 offsetPanelTop.add(_offsetSecBox);
193 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
194 offsetPanel.add(offsetPanelTop);
196 // radio buttons for photo / point later
197 JPanel offsetPanelBot = new JPanel();
198 offsetPanelBot.setLayout(new FlowLayout());
199 offsetPanelBot.setBorder(null);
200 _photoLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.photolater"));
201 _pointLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.pointlater"));
202 _photoLaterOption.addItemListener(optionsChangedListener);
203 _pointLaterOption.addItemListener(optionsChangedListener);
204 ButtonGroup laterGroup = new ButtonGroup();
205 laterGroup.add(_photoLaterOption);
206 laterGroup.add(_pointLaterOption);
207 offsetPanelBot.add(_photoLaterOption);
208 offsetPanelBot.add(_pointLaterOption);
209 offsetPanel.add(offsetPanelBot);
210 offsetPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
211 card2Top.add(offsetPanel);
213 // time limits section
214 JPanel limitsPanel = new JPanel();
215 limitsPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.limitspanel")));
216 limitsPanel.setLayout(new BoxLayout(limitsPanel, BoxLayout.Y_AXIS));
217 JPanel timeLimitPanel = new JPanel();
218 timeLimitPanel.setLayout(new FlowLayout());
219 JRadioButton noTimeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.notimelimit"));
220 noTimeLimitRadio.addItemListener(optionsChangedListener);
221 timeLimitPanel.add(noTimeLimitRadio);
222 _timeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.timelimit") + " : ");
223 _timeLimitRadio.addItemListener(optionsChangedListener);
224 timeLimitPanel.add(_timeLimitRadio);
225 groupRadioButtons(noTimeLimitRadio, _timeLimitRadio);
226 _limitMinBox = new JTextField(3);
227 _limitMinBox.addKeyListener(optionsChangedListener);
228 timeLimitPanel.add(_limitMinBox);
229 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
230 _limitSecBox = new JTextField(3);
231 _limitSecBox.addKeyListener(optionsChangedListener);
232 timeLimitPanel.add(_limitSecBox);
233 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
234 limitsPanel.add(timeLimitPanel);
236 JPanel distLimitPanel = new JPanel();
237 distLimitPanel.setLayout(new FlowLayout());
238 JRadioButton noDistLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.nodistancelimit"));
239 noDistLimitRadio.addItemListener(optionsChangedListener);
240 distLimitPanel.add(noDistLimitRadio);
241 _distLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.distancelimit"));
242 _distLimitRadio.addItemListener(optionsChangedListener);
243 distLimitPanel.add(_distLimitRadio);
244 groupRadioButtons(noDistLimitRadio, _distLimitRadio);
245 _limitDistBox = new JTextField(4);
246 _limitDistBox.addKeyListener(optionsChangedListener);
247 distLimitPanel.add(_limitDistBox);
248 String[] distUnitsOptions = {I18nManager.getText("units.kilometres"), I18nManager.getText("units.metres"),
249 I18nManager.getText("units.miles")};
250 _distUnitsDropdown = new JComboBox(distUnitsOptions);
251 _distUnitsDropdown.addItemListener(optionsChangedListener);
252 distLimitPanel.add(_distUnitsDropdown);
253 limitsPanel.add(distLimitPanel);
254 limitsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
255 card2Top.add(limitsPanel);
258 JButton previewButton = new JButton(I18nManager.getText("button.preview"));
259 previewButton.addActionListener(new ActionListener() {
260 public void actionPerformed(ActionEvent e)
265 card2Top.add(previewButton);
266 card2.add(card2Top, BorderLayout.NORTH);
268 _previewTable = new JTable(new PhotoPreviewTableModel());
269 JScrollPane previewScrollPane = new JScrollPane(_previewTable);
270 previewScrollPane.setPreferredSize(new Dimension(300, 100));
271 card2.add(previewScrollPane, BorderLayout.CENTER);
272 _cards.add(card2, "card2");
273 mainPanel.add(_cards, BorderLayout.CENTER);
275 // Button panel at the bottom
276 JPanel buttonPanel = new JPanel();
277 _backButton = new JButton(I18nManager.getText("button.back"));
278 _backButton.addActionListener(new ActionListener()
280 public void actionPerformed(ActionEvent e)
282 CardLayout cl = (CardLayout) _cards.getLayout();
284 _backButton.setEnabled(false);
285 _nextButton.setEnabled(true);
286 _okButton.setEnabled(false);
287 _previewEnabled = false;
290 _backButton.setEnabled(false);
291 buttonPanel.add(_backButton);
292 _nextButton = new JButton(I18nManager.getText("button.next"));
293 _nextButton.addActionListener(new ActionListener()
295 public void actionPerformed(ActionEvent e)
297 int rowNum = _photoSelectionTable.getSelectedRow();
298 if (rowNum < 0) {rowNum = 0;}
299 PhotoSelectionTableRow selectedRow = ((PhotoSelectionTableModel) _photoSelectionTable.getModel())
301 setupSecondCard(selectedRow.getTimeDiff());
304 buttonPanel.add(_nextButton);
305 _okButton = new JButton(I18nManager.getText("button.ok"));
306 _okButton.addActionListener(new ActionListener()
308 public void actionPerformed(ActionEvent e)
314 _okButton.setEnabled(false);
315 buttonPanel.add(_okButton);
316 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
317 cancelButton.addActionListener(new ActionListener()
319 public void actionPerformed(ActionEvent e)
324 buttonPanel.add(cancelButton);
325 mainPanel.add(buttonPanel, BorderLayout.SOUTH);
331 * Construct a table model for the photo selection table
332 * @param inTrackInfo track info object
333 * @return table model
335 private static PhotoSelectionTableModel makePhotoSelectionTableModel(TrackInfo inTrackInfo)
337 PhotoSelectionTableModel model = new PhotoSelectionTableModel();
338 int numPhotos = inTrackInfo.getPhotoList().getNumPhotos();
339 for (int i=0; i<numPhotos; i++)
341 Photo photo = inTrackInfo.getPhotoList().getPhoto(i);
342 // For working out time differences, can't use photos which already had point information
343 if (photo.getDataPoint() != null && photo.getDataPoint().hasTimestamp()
344 && photo.getOriginalStatus() == Photo.Status.NOT_CONNECTED)
346 // Calculate time difference, add to table model
347 long timeDiff = photo.getTimestamp().getSecondsSince(photo.getDataPoint().getTimestamp());
348 model.addPhoto(photo, timeDiff);
356 * Group the two radio buttons together with a ButtonGroup
357 * @param inButton1 first radio button
358 * @param inButton2 second radio button
360 private static void groupRadioButtons(JRadioButton inButton1, JRadioButton inButton2)
362 ButtonGroup buttonGroup = new ButtonGroup();
363 buttonGroup.add(inButton1);
364 buttonGroup.add(inButton2);
365 inButton1.setSelected(true);
370 * Set up the second card using the given time difference and show it
371 * @param inTimeDiff time difference to use for photo time offsets
373 private void setupSecondCard(TimeDifference inTimeDiff)
375 _previewEnabled = false;
376 boolean hasTimeDiff = inTimeDiff != null;
379 // No time difference available, so calculate based on computer's time zone
380 inTimeDiff = getTimezoneOffset();
382 // Use time difference to set edit boxes
383 _offsetHourBox.setText("" + inTimeDiff.getNumHours());
384 _offsetMinBox.setText("" + inTimeDiff.getNumMinutes());
385 _offsetSecBox.setText("" + inTimeDiff.getNumSeconds());
386 _photoLaterOption.setSelected(inTimeDiff.getIsPositive());
387 _pointLaterOption.setSelected(!inTimeDiff.getIsPositive());
388 createPreview(inTimeDiff, true);
389 CardLayout cl = (CardLayout) _cards.getLayout();
391 _backButton.setEnabled(hasTimeDiff);
392 _nextButton.setEnabled(false);
393 // enable ok button if any photos have been selected
394 _okButton.setEnabled(((PhotoPreviewTableModel) _previewTable.getModel()).hasPhotosSelected());
395 _previewEnabled = true;
400 * Create a preview of the correlate action using the selected time difference
401 * @param inFromButton true if triggered from button press, false if automatic
403 public void createPreview(boolean inFromButton)
405 // Exit if still on first panel
406 if (!_previewEnabled) {return;}
407 // Create a TimeDifference based on the edit boxes
408 int numHours = getValue(_offsetHourBox.getText());
409 int numMins = getValue(_offsetMinBox.getText());
410 int numSecs = getValue(_offsetSecBox.getText());
411 boolean isPos = _photoLaterOption.isSelected();
412 createPreview(new TimeDifference(numHours, numMins, numSecs, isPos), inFromButton);
417 * Create a preview of the correlate action using the selected time difference
418 * @param inTimeDiff TimeDifference to use for preview
419 * @param inShowWarning true to show warning if all points out of range
421 private void createPreview(TimeDifference inTimeDiff, boolean inShowWarning)
423 TimeDifference timeLimit = parseTimeLimit();
424 double angDistLimit = parseDistanceLimit();
425 PhotoPreviewTableModel model = new PhotoPreviewTableModel();
426 PhotoList photos = _app.getTrackInfo().getPhotoList();
427 // Loop through photos deciding whether to set correlate flag or not
428 int numPhotos = photos.getNumPhotos();
429 for (int i=0; i<numPhotos; i++)
431 Photo photo = photos.getPhoto(i);
432 PointPair pair = getPointPairForPhoto(_app.getTrackInfo().getTrack(), photo, inTimeDiff);
433 PhotoPreviewTableRow row = new PhotoPreviewTableRow(pair);
434 // Don't try to correlate photos which don't have points either side
435 boolean correlatePhoto = pair.isValid();
436 // Don't select photos which already have a point
437 if (photo.getCurrentStatus() != Photo.Status.NOT_CONNECTED) {correlatePhoto = false;}
438 // Check time limits, distance limits
439 if (timeLimit != null && correlatePhoto) {
440 long numSecs = pair.getMinSeconds();
441 correlatePhoto = (numSecs <= timeLimit.getTotalSeconds());
443 if (angDistLimit > 0.0 && correlatePhoto)
445 final double angDistPair = DataPoint.calculateRadiansBetween(pair.getPointBefore(), pair.getPointAfter());
446 double frac = pair.getFraction();
447 if (frac > 0.5) {frac = 1 - frac;}
448 final double angDistPhoto = angDistPair * frac;
449 correlatePhoto = (angDistPhoto < angDistLimit);
451 // Don't select photos which are already correlated to the same point
452 if (pair.getSecondsBefore() == 0L && pair.getPointBefore().isDuplicate(photo.getDataPoint())) {
453 correlatePhoto = false;
455 row.setCorrelateFlag(correlatePhoto);
456 model.addPhotoRow(row);
458 _previewTable.setModel(model);
459 // Set distance units
460 model.setDistanceUnits(getSelectedDistanceUnits());
462 _previewTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
463 final int[] colWidths = {150, 160, 100, 100, 50};
464 for (int i=0; i<model.getColumnCount(); i++) {
465 _previewTable.getColumnModel().getColumn(i).setPreferredWidth(colWidths[i]);
467 // check if any photos found
468 _okButton.setEnabled(model.hasPhotosSelected());
469 if (inShowWarning && !model.hasPhotosSelected())
471 JOptionPane.showMessageDialog(_dialog, I18nManager.getText("dialog.correlate.alloutsiderange"),
472 I18nManager.getText(getNameKey()), JOptionPane.ERROR_MESSAGE);
477 * Parse the time limit values entered and validate them
478 * @return TimeDifference object describing limit
480 private TimeDifference parseTimeLimit()
482 if (!_timeLimitRadio.isSelected()) {return null;}
483 int mins = getValue(_limitMinBox.getText());
484 _limitMinBox.setText("" + mins);
485 int secs = getValue(_limitSecBox.getText());
486 _limitSecBox.setText("" + secs);
487 if (mins <= 0 && secs <= 0) {return null;}
488 return new TimeDifference(0, mins, secs, true);
492 * Parse the distance limit value entered and validate
493 * @return angular distance in radians
495 private double parseDistanceLimit()
498 if (_distLimitRadio.isSelected())
502 value = Double.parseDouble(_limitDistBox.getText());
504 catch (NumberFormatException nfe) {}
507 _limitDistBox.setText("0");
510 _limitDistBox.setText("" + value);
511 return Distance.convertDistanceToRadians(value, getSelectedDistanceUnits());
516 * @return the selected distance units from the dropdown
518 private Distance.Units getSelectedDistanceUnits()
520 final Distance.Units[] distUnits = {Distance.Units.KILOMETRES, Distance.Units.METRES, Distance.Units.MILES};
521 return distUnits[_distUnitsDropdown.getSelectedIndex()];
526 * Try to parse the given string
527 * @param inText String to parse
528 * @return value if parseable, 0 otherwise
530 private static int getValue(String inText)
534 value = Integer.parseInt(inText);
536 catch (NumberFormatException nfe) {}
542 * Get the point pair surrounding the given photo
543 * @param inTrack track object
544 * @param inPhoto photo object
545 * @param inOffset time offset to apply to photos
546 * @return point pair resulting from correlation
548 private static PointPair getPointPairForPhoto(Track inTrack, Photo inPhoto, TimeDifference inOffset)
550 PointPair pair = new PointPair(inPhoto);
551 // Add/subtract offet to photo timestamp
552 Timestamp photoStamp = inPhoto.getTimestamp().createMinusOffset(inOffset);
553 int numPoints = inTrack.getNumPoints();
554 for (int i=0; i<numPoints; i++)
556 DataPoint point = inTrack.getPoint(i);
557 if (point.getPhoto() == null || point.getPhoto().getCurrentStatus() != Photo.Status.TAGGED)
559 Timestamp pointStamp = point.getTimestamp();
560 if (pointStamp != null && pointStamp.isValid())
562 long numSeconds = pointStamp.getSecondsSince(photoStamp);
563 pair.addPoint(point, numSeconds);
572 * Construct an array of the point pairs to use for correlation
573 * @return array of PointPair objects
575 private PointPair[] getPointPairs()
577 PhotoPreviewTableModel model = (PhotoPreviewTableModel) _previewTable.getModel();
578 int numPhotos = model.getRowCount();
579 PointPair[] pairs = new PointPair[numPhotos];
580 // Loop over photos in preview table model
581 for (int i=0; i<numPhotos; i++)
583 PhotoPreviewTableRow row = model.getRow(i);
584 // add all selected pairs to array (other elements remain null)
585 if (row.getCorrelateFlag().booleanValue())
587 pairs[i] = row.getPointPair();
594 * @return time difference of local time zone from UTC when the first photo was taken
596 private TimeDifference getTimezoneOffset()
599 // Base time difference on DST when first photo was taken
600 Photo firstPhoto = _app.getTrackInfo().getPhotoList().getPhoto(0);
601 if (firstPhoto != null && firstPhoto.getTimestamp() != null) {
602 cal = firstPhoto.getTimestamp().getCalendar();
605 // No photo or no timestamp, just use current time
606 cal = Calendar.getInstance();
608 // Both time zone offset and dst offset are based on milliseconds, so convert to seconds
609 TimeDifference timeDiff = new TimeDifference((cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 1000);
615 * Calculate the median index to select from the table
616 * @param inModel table model
617 * @return index of entry to select from table
619 private static int getMedianIndex(PhotoSelectionTableModel inModel)
621 // make sortable list
622 TreeSet<TimeIndexPair> set = new TreeSet<TimeIndexPair>();
623 // loop through rows of table adding to list
624 int numRows = inModel.getRowCount();
626 for (i=0; i<numRows; i++)
628 PhotoSelectionTableRow row = inModel.getRow(i);
629 set.add(new TimeIndexPair(row.getTimeDiff().getTotalSeconds(), i));
631 // pull out middle entry and return index
632 TimeIndexPair pair = null;
633 Iterator<TimeIndexPair> iterator = set.iterator();
634 for (i=0; i<(numRows+1)/2; i++)
636 pair = iterator.next();
638 return pair.getIndex();
643 * Disable the ok button
645 public void disableOkButton()
647 if (_okButton != null) {
648 _okButton.setEnabled(false);
654 * Check if the track has any uncorrelated photos
655 * @return true if there are any photos which are not connected to points
657 private boolean trackHasUncorrelatedPhotos()
659 PhotoList photoList = _app.getTrackInfo().getPhotoList();
660 int numPhotos = photoList.getNumPhotos();
662 for (int i=0; i<numPhotos; i++)
664 Photo photo = photoList.getPhoto(i);
665 if (photo != null && photo.getDataPoint() == null) {
669 // no uncorrelated photos found
674 * Finish the correlation by modifying the track
675 * and passing the Undo information back to the App
677 private void finishCorrelation()
679 PointPair[] pointPairs = getPointPairs();
680 if (pointPairs == null || pointPairs.length <= 0) {return;}
682 // begin to construct undo information
683 UndoCorrelatePhotos undo = new UndoCorrelatePhotos(_app.getTrackInfo());
685 int arraySize = pointPairs.length;
686 int i = 0, numPhotos = 0;
687 int numPointsToCreate = 0;
688 PointPair pair = null;
689 for (i=0; i<arraySize; i++)
691 pair = pointPairs[i];
692 if (pair != null && pair.isValid())
694 if (pair.getMinSeconds() == 0L)
697 Photo pointPhoto = pair.getPointBefore().getPhoto();
698 if (pointPhoto == null)
700 // photo coincides with photoless point so connect the two
701 pair.getPointBefore().setPhoto(pair.getPhoto());
702 pair.getPhoto().setDataPoint(pair.getPointBefore());
704 else if (pointPhoto.equals(pair.getPhoto())) {
705 // photo is already connected, nothing to do
708 // point is already connected to a different photo, so need to clone point
714 // photo time falls between two points, so need to interpolate new one
720 // Second loop, to create points if necessary
721 if (numPointsToCreate > 0)
723 // make new array for added points
724 DataPoint[] addedPoints = new DataPoint[numPointsToCreate];
726 DataPoint pointToAdd = null;
727 for (i=0; i<arraySize; i++)
729 pair = pointPairs[i];
730 if (pair != null && pair.isValid())
733 if (pair.getMinSeconds() == 0L && pair.getPointBefore().getPhoto() != null
734 && !pair.getPointBefore().getPhoto().equals(pair.getPhoto()))
737 pointToAdd = pair.getPointBefore().clonePoint();
739 else if (pair.getMinSeconds() > 0L)
742 pointToAdd = DataPoint.interpolate(pair.getPointBefore(), pair.getPointAfter(), pair.getFraction());
744 if (pointToAdd != null)
746 // link photo to point
747 pointToAdd.setPhoto(pair.getPhoto());
748 pair.getPhoto().setDataPoint(pointToAdd);
749 // set to start of segment so not joined in track
750 pointToAdd.setSegmentStart(true);
751 // add to point array
752 addedPoints[pointNum] = pointToAdd;
758 _app.getTrackInfo().getTrack().appendPoints(addedPoints);
761 // send undo information back to controlling app
762 undo.setNumPhotosCorrelated(numPhotos);
763 _app.completeFunction(undo, ("" + numPhotos + " "
764 + (numPhotos==1?I18nManager.getText("confirm.correlate.single"):I18nManager.getText("confirm.correlate.multi"))));
765 // observers already informed by track update