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.JFrame;
21 import javax.swing.JLabel;
22 import javax.swing.JOptionPane;
23 import javax.swing.JPanel;
24 import javax.swing.JRadioButton;
25 import javax.swing.JScrollPane;
26 import javax.swing.JTable;
27 import javax.swing.JTextField;
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;
42 * Class to manage the automatic correlation of photos to points
43 * including the GUI stuff to control the correlation options
45 public class PhotoCorrelator
48 private JFrame _parentFrame;
49 private JDialog _dialog;
50 private JButton _nextButton = null, _backButton = null;
51 private JButton _okButton = null;
52 private JPanel _cards = null;
53 private JTable _photoSelectionTable = null;
54 private JLabel _tipLabel = null;
55 private JTextField _offsetHourBox = null, _offsetMinBox = null, _offsetSecBox = null;
56 private JRadioButton _photoLaterOption = null, _pointLaterOption = null;
57 private JRadioButton _timeLimitRadio = null, _distLimitRadio = null;
58 private JTextField _limitMinBox = null, _limitSecBox = null;
59 private JTextField _limitDistBox = null;
60 private JComboBox _distUnitsDropdown = null;
61 private JTable _previewTable = null;
62 private boolean _firstTabAvailable = false;
63 private boolean _previewEnabled = false; // flag required to enable preview function on second panel
68 * @param inApp App object to report actions to
69 * @param inFrame parent frame for dialogs
71 public PhotoCorrelator(App inApp, JFrame inFrame)
74 _parentFrame = inFrame;
75 _dialog = new JDialog(inFrame, I18nManager.getText("dialog.correlate.title"), true);
76 _dialog.setLocationRelativeTo(inFrame);
77 _dialog.getContentPane().add(makeDialogContents());
83 * Reset dialog and show it
87 // Check whether track has timestamps, exit if not
88 if (!_app.getTrackInfo().getTrack().hasData(Field.TIMESTAMP))
90 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.correlate.notimestamps"),
91 I18nManager.getText("dialog.correlate.title"), JOptionPane.INFORMATION_MESSAGE);
94 // Check for any non-correlated photos, show warning continue/cancel
95 if (!trackHasUncorrelatedPhotos())
97 Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")};
98 if (JOptionPane.showOptionDialog(_parentFrame, I18nManager.getText("dialog.correlate.nouncorrelatedphotos"),
99 I18nManager.getText("dialog.correlate.title"), JOptionPane.YES_NO_OPTION,
100 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
101 == JOptionPane.NO_OPTION)
106 PhotoSelectionTableModel model = makePhotoSelectionTableModel(_app.getTrackInfo());
107 _firstTabAvailable = model != null && model.getRowCount() > 0;
108 CardLayout cl = (CardLayout) _cards.getLayout();
109 if (_firstTabAvailable)
112 _nextButton.setEnabled(true);
113 _backButton.setEnabled(false);
114 _tipLabel.setVisible(false);
115 _photoSelectionTable.setModel(model);
116 _previewEnabled = false;
117 for (int i=0; i<model.getColumnCount(); i++) {
118 _photoSelectionTable.getColumnModel().getColumn(i).setPreferredWidth(i==3?50:150);
120 // Calculate median time difference, select corresponding row of table
121 int preselectedIndex = model.getRowCount() < 3 ? 0 : getMedianIndex(model);
122 _photoSelectionTable.getSelectionModel().setSelectionInterval(preselectedIndex, preselectedIndex);
123 _nextButton.requestFocus();
127 _tipLabel.setVisible(true);
128 setupSecondCard(null);
135 * Make contents of correlate dialog
136 * @return JPanel containing gui elements
138 private JPanel makeDialogContents()
140 JPanel mainPanel = new JPanel();
141 mainPanel.setLayout(new BorderLayout());
142 // Card panel in the middle
143 _cards = new JPanel();
144 _cards.setLayout(new CardLayout());
146 // First panel for photo selection table
147 JPanel card1 = new JPanel();
148 card1.setLayout(new BorderLayout(10, 10));
149 card1.add(new JLabel(I18nManager.getText("dialog.correlate.photoselect.intro")), BorderLayout.NORTH);
150 _photoSelectionTable = new JTable();
151 JScrollPane photoScrollPane = new JScrollPane(_photoSelectionTable);
152 photoScrollPane.setPreferredSize(new Dimension(400, 100));
153 card1.add(photoScrollPane, BorderLayout.CENTER);
154 _cards.add(card1, "card1");
156 OptionsChangedListener optionsChangedListener = new OptionsChangedListener(this);
157 // Second panel for options
158 JPanel card2 = new JPanel();
159 card2.setLayout(new BorderLayout());
160 JPanel card2Top = new JPanel();
161 card2Top.setLayout(new BoxLayout(card2Top, BoxLayout.Y_AXIS));
162 _tipLabel = new JLabel(I18nManager.getText("dialog.correlate.options.tip"));
163 card2Top.add(_tipLabel);
164 card2Top.add(new JLabel(I18nManager.getText("dialog.correlate.options.intro")));
165 // time offset section
166 JPanel offsetPanel = new JPanel();
167 offsetPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.offsetpanel")));
168 offsetPanel.setLayout(new BoxLayout(offsetPanel, BoxLayout.Y_AXIS));
169 JPanel offsetPanelTop = new JPanel();
170 offsetPanelTop.setLayout(new FlowLayout());
171 offsetPanelTop.setBorder(null);
172 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset") + ": "));
173 _offsetHourBox = new JTextField(3);
174 _offsetHourBox.addKeyListener(optionsChangedListener);
175 offsetPanelTop.add(_offsetHourBox);
176 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.hours")));
177 _offsetMinBox = new JTextField(3);
178 _offsetMinBox.addKeyListener(optionsChangedListener);
179 offsetPanelTop.add(_offsetMinBox);
180 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
181 _offsetSecBox = new JTextField(3);
182 _offsetSecBox.addKeyListener(optionsChangedListener);
183 offsetPanelTop.add(_offsetSecBox);
184 offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
185 offsetPanel.add(offsetPanelTop);
187 // radio buttons for photo / point later
188 JPanel offsetPanelBot = new JPanel();
189 offsetPanelBot.setLayout(new FlowLayout());
190 offsetPanelBot.setBorder(null);
191 _photoLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.photolater"));
192 _pointLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.pointlater"));
193 _photoLaterOption.addItemListener(optionsChangedListener);
194 _pointLaterOption.addItemListener(optionsChangedListener);
195 ButtonGroup laterGroup = new ButtonGroup();
196 laterGroup.add(_photoLaterOption);
197 laterGroup.add(_pointLaterOption);
198 offsetPanelBot.add(_photoLaterOption);
199 offsetPanelBot.add(_pointLaterOption);
200 offsetPanel.add(offsetPanelBot);
201 offsetPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
202 card2Top.add(offsetPanel);
204 // time limits section
205 JPanel limitsPanel = new JPanel();
206 limitsPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.limitspanel")));
207 limitsPanel.setLayout(new BoxLayout(limitsPanel, BoxLayout.Y_AXIS));
208 JPanel timeLimitPanel = new JPanel();
209 timeLimitPanel.setLayout(new FlowLayout());
210 JRadioButton noTimeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.notimelimit"));
211 noTimeLimitRadio.addItemListener(optionsChangedListener);
212 timeLimitPanel.add(noTimeLimitRadio);
213 _timeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.timelimit") + " : ");
214 _timeLimitRadio.addItemListener(optionsChangedListener);
215 timeLimitPanel.add(_timeLimitRadio);
216 groupRadioButtons(noTimeLimitRadio, _timeLimitRadio);
217 _limitMinBox = new JTextField(3);
218 _limitMinBox.addKeyListener(optionsChangedListener);
219 timeLimitPanel.add(_limitMinBox);
220 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
221 _limitSecBox = new JTextField(3);
222 _limitSecBox.addKeyListener(optionsChangedListener);
223 timeLimitPanel.add(_limitSecBox);
224 timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
225 limitsPanel.add(timeLimitPanel);
227 JPanel distLimitPanel = new JPanel();
228 distLimitPanel.setLayout(new FlowLayout());
229 JRadioButton noDistLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.nodistancelimit"));
230 noDistLimitRadio.addItemListener(optionsChangedListener);
231 distLimitPanel.add(noDistLimitRadio);
232 _distLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.distancelimit"));
233 _distLimitRadio.addItemListener(optionsChangedListener);
234 distLimitPanel.add(_distLimitRadio);
235 groupRadioButtons(noDistLimitRadio, _distLimitRadio);
236 _limitDistBox = new JTextField(4);
237 _limitDistBox.addKeyListener(optionsChangedListener);
238 distLimitPanel.add(_limitDistBox);
239 String[] distUnitsOptions = {I18nManager.getText("units.kilometres"), I18nManager.getText("units.metres"),
240 I18nManager.getText("units.miles")};
241 _distUnitsDropdown = new JComboBox(distUnitsOptions);
242 _distUnitsDropdown.addItemListener(optionsChangedListener);
243 distLimitPanel.add(_distUnitsDropdown);
244 limitsPanel.add(distLimitPanel);
245 limitsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
246 card2Top.add(limitsPanel);
249 JButton previewButton = new JButton(I18nManager.getText("button.preview"));
250 previewButton.addActionListener(new ActionListener() {
251 public void actionPerformed(ActionEvent e)
256 card2Top.add(previewButton);
257 card2.add(card2Top, BorderLayout.NORTH);
259 _previewTable = new JTable();
260 JScrollPane previewScrollPane = new JScrollPane(_previewTable);
261 previewScrollPane.setPreferredSize(new Dimension(300, 100));
262 card2.add(previewScrollPane, BorderLayout.CENTER);
263 _cards.add(card2, "card2");
264 mainPanel.add(_cards, BorderLayout.CENTER);
266 // Button panel at the bottom
267 JPanel buttonPanel = new JPanel();
268 _backButton = new JButton(I18nManager.getText("button.back"));
269 _backButton.addActionListener(new ActionListener()
271 public void actionPerformed(ActionEvent e)
273 CardLayout cl = (CardLayout) _cards.getLayout();
275 _backButton.setEnabled(false);
276 _nextButton.setEnabled(true);
277 _okButton.setEnabled(false);
278 _previewEnabled = false;
281 _backButton.setEnabled(false);
282 buttonPanel.add(_backButton);
283 _nextButton = new JButton(I18nManager.getText("button.next"));
284 _nextButton.addActionListener(new ActionListener()
286 public void actionPerformed(ActionEvent e)
288 int rowNum = _photoSelectionTable.getSelectedRow();
289 if (rowNum < 0) {rowNum = 0;}
290 PhotoSelectionTableRow selectedRow = ((PhotoSelectionTableModel) _photoSelectionTable.getModel())
292 setupSecondCard(selectedRow.getTimeDiff());
295 buttonPanel.add(_nextButton);
296 _okButton = new JButton(I18nManager.getText("button.ok"));
297 _okButton.addActionListener(new ActionListener()
299 public void actionPerformed(ActionEvent e)
301 _app.finishCorrelatePhotos(getPointPairs());
305 _okButton.setEnabled(false);
306 buttonPanel.add(_okButton);
307 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
308 cancelButton.addActionListener(new ActionListener()
310 public void actionPerformed(ActionEvent e)
315 buttonPanel.add(cancelButton);
316 mainPanel.add(buttonPanel, BorderLayout.SOUTH);
322 * Construct a table model for the photo selection table
323 * @param inTrackInfo track info object
324 * @return table model
326 private static PhotoSelectionTableModel makePhotoSelectionTableModel(TrackInfo inTrackInfo)
328 PhotoSelectionTableModel model = new PhotoSelectionTableModel();
329 int numPhotos = inTrackInfo.getPhotoList().getNumPhotos();
330 for (int i=0; i<numPhotos; i++)
332 Photo photo = inTrackInfo.getPhotoList().getPhoto(i);
333 if (photo.getDataPoint() != null && photo.getDataPoint().hasTimestamp())
335 // Calculate time difference, add to table model
336 long timeDiff = photo.getTimestamp().getSecondsSince(photo.getDataPoint().getTimestamp());
337 model.addPhoto(photo, timeDiff);
345 * Group the two radio buttons together with a ButtonGroup
346 * @param inButton1 first radio button
347 * @param inButton2 second radio button
349 private static void groupRadioButtons(JRadioButton inButton1, JRadioButton inButton2)
351 ButtonGroup buttonGroup = new ButtonGroup();
352 buttonGroup.add(inButton1);
353 buttonGroup.add(inButton2);
354 inButton1.setSelected(true);
359 * Set up the second card using the given time difference and show it
360 * @param inTimeDiff time difference to use for photo time offsets
362 private void setupSecondCard(TimeDifference inTimeDiff)
364 _previewEnabled = false;
365 boolean hasTimeDiff = inTimeDiff != null;
368 // No time difference available, so calculate based on computer's time zone
369 inTimeDiff = getTimezoneOffset();
371 // Use time difference to set edit boxes
372 _offsetHourBox.setText("" + inTimeDiff.getNumHours());
373 _offsetMinBox.setText("" + inTimeDiff.getNumMinutes());
374 _offsetSecBox.setText("" + inTimeDiff.getNumSeconds());
375 _photoLaterOption.setSelected(inTimeDiff.getIsPositive());
376 _pointLaterOption.setSelected(!inTimeDiff.getIsPositive());
377 createPreview(inTimeDiff, true);
378 CardLayout cl = (CardLayout) _cards.getLayout();
380 _backButton.setEnabled(hasTimeDiff);
381 _nextButton.setEnabled(false);
382 // enable ok button if any photos have been selected
383 _okButton.setEnabled(((PhotoPreviewTableModel) _previewTable.getModel()).hasPhotosSelected());
384 _previewEnabled = true;
389 * Create a preview of the correlate action using the selected time difference
390 * @param inFromButton true if triggered from button press, false if automatic
392 public void createPreview(boolean inFromButton)
394 // Exit if still on first panel
395 if (!_previewEnabled) {return;}
396 // Create a TimeDifference based on the edit boxes
397 int numHours = getValue(_offsetHourBox.getText());
398 int numMins = getValue(_offsetMinBox.getText());
399 int numSecs = getValue(_offsetSecBox.getText());
400 boolean isPos = _photoLaterOption.isSelected();
401 createPreview(new TimeDifference(numHours, numMins, numSecs, isPos), inFromButton);
406 * Create a preview of the correlate action using the selected time difference
407 * @param inTimeDiff TimeDifference to use for preview
408 * @param inShowWarning true to show warning if all points out of range
410 private void createPreview(TimeDifference inTimeDiff, boolean inShowWarning)
412 TimeDifference timeLimit = parseTimeLimit();
413 double angDistLimit = parseDistanceLimit();
414 PhotoPreviewTableModel model = new PhotoPreviewTableModel();
415 PhotoList photos = _app.getTrackInfo().getPhotoList();
416 // Loop through photos deciding whether to set correlate flag or not
417 int numPhotos = photos.getNumPhotos();
418 for (int i=0; i<numPhotos; i++)
420 Photo photo = photos.getPhoto(i);
421 PointPair pair = getPointPairForPhoto(_app.getTrackInfo().getTrack(), photo, inTimeDiff);
422 PhotoPreviewTableRow row = new PhotoPreviewTableRow(pair);
423 // Don't try to correlate photos which don't have points either side
424 boolean correlatePhoto = pair.isValid();
425 // Check time limits, distance limits
426 if (timeLimit != null && correlatePhoto) {
427 long numSecs = pair.getMinSeconds();
428 correlatePhoto = (numSecs <= timeLimit.getTotalSeconds());
430 if (angDistLimit > 0.0 && correlatePhoto)
432 final double angDistPair = DataPoint.calculateRadiansBetween(pair.getPointBefore(), pair.getPointAfter());
433 double frac = pair.getFraction();
434 if (frac > 0.5) {frac = 1 - frac;}
435 final double angDistPhoto = angDistPair * frac;
436 correlatePhoto = (angDistPhoto < angDistLimit);
438 // Don't select photos which are already correlated to the same point
439 if (pair.getSecondsBefore() == 0L && pair.getPointBefore().getPhoto() != null
440 && pair.getPointBefore().getPhoto().equals(photo)) {
441 correlatePhoto = false;
443 row.setCorrelateFlag(correlatePhoto);
444 model.addPhotoRow(row);
446 _previewTable.setModel(model);
447 // Set distance units
448 model.setDistanceUnits(getSelectedDistanceUnits());
450 _previewTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
451 final int[] colWidths = {150, 160, 100, 100, 50};
452 for (int i=0; i<model.getColumnCount(); i++) {
453 _previewTable.getColumnModel().getColumn(i).setPreferredWidth(colWidths[i]);
455 // check if any photos found
456 _okButton.setEnabled(model.hasPhotosSelected());
457 if (inShowWarning && !model.hasPhotosSelected())
459 JOptionPane.showMessageDialog(_dialog, I18nManager.getText("dialog.correlate.alloutsiderange"),
460 I18nManager.getText("dialog.correlate.title"), JOptionPane.ERROR_MESSAGE);
465 * Parse the time limit values entered and validate them
466 * @return TimeDifference object describing limit
468 private TimeDifference parseTimeLimit()
470 if (!_timeLimitRadio.isSelected()) {return null;}
471 int mins = getValue(_limitMinBox.getText());
472 _limitMinBox.setText("" + mins);
473 int secs = getValue(_limitSecBox.getText());
474 _limitSecBox.setText("" + secs);
475 if (mins <= 0 && secs <= 0) {return null;}
476 return new TimeDifference(0, mins, secs, true);
480 * Parse the distance limit value entered and validate
481 * @return angular distance in radians
483 private double parseDistanceLimit()
486 if (_distLimitRadio.isSelected())
490 value = Double.parseDouble(_limitDistBox.getText());
492 catch (NumberFormatException nfe) {}
495 _limitDistBox.setText("0");
498 _limitDistBox.setText("" + value);
499 return Distance.convertDistanceToRadians(value, getSelectedDistanceUnits());
504 * @return the selected distance units from the dropdown
506 private int getSelectedDistanceUnits()
508 final int[] distUnits = {Distance.UNITS_KILOMETRES, Distance.UNITS_METRES, Distance.UNITS_MILES};
509 return distUnits[_distUnitsDropdown.getSelectedIndex()];
514 * Try to parse the given string
515 * @param inText String to parse
516 * @return value if parseable, 0 otherwise
518 private static int getValue(String inText)
522 value = Integer.parseInt(inText);
524 catch (NumberFormatException nfe) {}
530 * Get the point pair surrounding the given photo
531 * @param inTrack track object
532 * @param inPhoto photo object
533 * @param inOffset time offset to apply to photos
534 * @return point pair resulting from correlation
536 private static PointPair getPointPairForPhoto(Track inTrack, Photo inPhoto, TimeDifference inOffset)
538 PointPair pair = new PointPair(inPhoto);
539 // Add/subtract offet to photo timestamp
540 Timestamp photoStamp = inPhoto.getTimestamp().createMinusOffset(inOffset);
541 int numPoints = inTrack.getNumPoints();
542 for (int i=0; i<numPoints; i++)
544 DataPoint point = inTrack.getPoint(i);
545 Timestamp pointStamp = point.getTimestamp();
546 if (pointStamp != null && pointStamp.isValid())
548 long numSeconds = pointStamp.getSecondsSince(photoStamp);
549 pair.addPoint(point, numSeconds);
557 * Construct an array of the point pairs to use for correlation
558 * @return array of PointPair objects
560 private PointPair[] getPointPairs()
562 PhotoPreviewTableModel model = (PhotoPreviewTableModel) _previewTable.getModel();
563 int numPhotos = model.getRowCount();
564 PointPair[] pairs = new PointPair[numPhotos];
565 // Loop over photos in preview table model
566 for (int i=0; i<numPhotos; i++)
568 PhotoPreviewTableRow row = model.getRow(i);
569 // add all selected pairs to array (other elements remain null)
570 if (row.getCorrelateFlag().booleanValue())
572 pairs[i] = row.getPointPair();
579 * @return time difference of local time zone from UTC when the first photo was taken
581 private TimeDifference getTimezoneOffset()
584 // Base time difference on DST when first photo was taken
585 Photo firstPhoto = _app.getTrackInfo().getPhotoList().getPhoto(0);
586 if (firstPhoto != null && firstPhoto.getTimestamp() != null) {
587 cal = firstPhoto.getTimestamp().getCalendar();
590 // No photo or no timestamp, just use current time
591 cal = Calendar.getInstance();
593 // Both time zone offset and dst offset are based on milliseconds, so convert to seconds
594 TimeDifference timeDiff = new TimeDifference((cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 1000);
600 * Calculate the median index to select from the table
601 * @param inModel table model
602 * @return index of entry to select from table
604 private static int getMedianIndex(PhotoSelectionTableModel inModel)
606 // make sortable list
607 TreeSet set = new TreeSet();
608 // loop through rows of table adding to list
609 int numRows = inModel.getRowCount();
611 for (i=0; i<numRows; i++)
613 PhotoSelectionTableRow row = inModel.getRow(i);
614 set.add(new TimeIndexPair(row.getTimeDiff().getTotalSeconds(), i));
616 // pull out middle entry and return index
617 TimeIndexPair pair = null;
618 Iterator iterator = set.iterator();
619 for (i=0; i<(numRows+1)/2; i++)
621 pair = (TimeIndexPair) iterator.next();
623 return pair.getIndex();
628 * Disable the ok button
630 public void disableOkButton()
632 if (_okButton != null) {
633 _okButton.setEnabled(false);
639 * Check if the track has any uncorrelated photos
640 * @return true if there are any photos which are not connected to points
642 private boolean trackHasUncorrelatedPhotos()
644 PhotoList photoList = _app.getTrackInfo().getPhotoList();
645 int numPhotos = photoList.getNumPhotos();
647 for (int i=0; i<numPhotos; i++)
649 Photo photo = photoList.getPhoto(i);
650 if (photo != null && photo.getDataPoint() == null) {
654 // no uncorrelated photos found