File: PhotoCorrelator.java

package info (click to toggle)
gpsprune 10-1
  • links: PTS, VCS
  • area: main
  • in suites: squeeze
  • size: 2,220 kB
  • ctags: 3,013
  • sloc: java: 22,662; sh: 23; makefile: 16; python: 15
file content (760 lines) | stat: -rw-r--r-- 26,039 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
package tim.prune.correlate;

import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Calendar;
import java.util.Iterator;
import java.util.TreeSet;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;

import tim.prune.App;
import tim.prune.GenericFunction;
import tim.prune.I18nManager;
import tim.prune.data.DataPoint;
import tim.prune.data.Distance;
import tim.prune.data.Field;
import tim.prune.data.Photo;
import tim.prune.data.PhotoList;
import tim.prune.data.TimeDifference;
import tim.prune.data.Timestamp;
import tim.prune.data.Track;
import tim.prune.data.TrackInfo;
import tim.prune.undo.UndoCorrelatePhotos;

/**
 * Class to manage the automatic correlation of photos to points
 * including the GUI stuff to control the correlation options
 */
public class PhotoCorrelator extends GenericFunction
{
	private JDialog _dialog;
	private JButton _nextButton = null, _backButton = null;
	private JButton _okButton = null;
	private JPanel _cards = null;
	private JTable _photoSelectionTable = null;
	private JLabel _tipLabel = null;
	private JTextField _offsetHourBox = null, _offsetMinBox = null, _offsetSecBox = null;
	private JRadioButton _photoLaterOption = null, _pointLaterOption = null;
	private JRadioButton _timeLimitRadio = null, _distLimitRadio = null;
	private JTextField _limitMinBox = null, _limitSecBox = null;
	private JTextField _limitDistBox = null;
	private JComboBox _distUnitsDropdown = null;
	private JTable _previewTable = null;
	private boolean _firstTabAvailable = false;
	private boolean _previewEnabled = false; // flag required to enable preview function on second panel


	/**
	 * Constructor
	 * @param inApp App object to report actions to
	 */
	public PhotoCorrelator(App inApp)
	{
		super(inApp);
	}


	/** Get the name key */
	public String getNameKey() {
		return "function.correlatephotos";
	}

	/**
	 * Reset dialog and show it
	 */
	public void begin()
	{
		// First create dialog if necessary
		if (_dialog == null)
		{
			_dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
			_dialog.setLocationRelativeTo(_parentFrame);
			_dialog.getContentPane().add(makeDialogContents());
			_dialog.pack();
		}
		// Check whether track has timestamps, exit if not
		if (!_app.getTrackInfo().getTrack().hasData(Field.TIMESTAMP))
		{
			JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.correlate.notimestamps"),
				I18nManager.getText(getNameKey()), JOptionPane.INFORMATION_MESSAGE);
			return;
		}
		// Check for any non-correlated photos, show warning continue/cancel
		if (!trackHasUncorrelatedPhotos())
		{
			Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")};
			if (JOptionPane.showOptionDialog(_parentFrame, I18nManager.getText("dialog.correlate.nouncorrelatedphotos"),
					I18nManager.getText(getNameKey()), JOptionPane.YES_NO_OPTION,
					JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
				== JOptionPane.NO_OPTION)
			{
				return;
			}
		}
		PhotoSelectionTableModel model = makePhotoSelectionTableModel(_app.getTrackInfo());
		_firstTabAvailable = model != null && model.getRowCount() > 0;
		CardLayout cl = (CardLayout) _cards.getLayout();
		if (_firstTabAvailable)
		{
			cl.first(_cards);
			_nextButton.setEnabled(true);
			_backButton.setEnabled(false);
			_tipLabel.setVisible(false);
			_photoSelectionTable.setModel(model);
			_previewEnabled = false;
			for (int i=0; i<model.getColumnCount(); i++) {
				_photoSelectionTable.getColumnModel().getColumn(i).setPreferredWidth(i==3?50:150);
			}
			// Calculate median time difference, select corresponding row of table
			int preselectedIndex = model.getRowCount() < 3 ? 0 : getMedianIndex(model);
			_photoSelectionTable.getSelectionModel().setSelectionInterval(preselectedIndex, preselectedIndex);
			_nextButton.requestFocus();
		}
		else
		{
			_tipLabel.setVisible(true);
			setupSecondCard(null);
		}
		_dialog.setVisible(true);
	}


	/**
	 * Make contents of correlate dialog
	 * @return JPanel containing gui elements
	 */
	private JPanel makeDialogContents()
	{
		JPanel mainPanel = new JPanel();
		mainPanel.setLayout(new BorderLayout());
		// Card panel in the middle
		_cards = new JPanel();
		_cards.setLayout(new CardLayout());

		// First panel for photo selection table
		JPanel card1 = new JPanel();
		card1.setLayout(new BorderLayout(10, 10));
		card1.add(new JLabel(I18nManager.getText("dialog.correlate.photoselect.intro")), BorderLayout.NORTH);
		_photoSelectionTable = new JTable();
		JScrollPane photoScrollPane = new JScrollPane(_photoSelectionTable);
		photoScrollPane.setPreferredSize(new Dimension(400, 100));
		card1.add(photoScrollPane, BorderLayout.CENTER);
		_cards.add(card1, "card1");

		OptionsChangedListener optionsChangedListener = new OptionsChangedListener(this);
		// Second panel for options
		JPanel card2 = new JPanel();
		card2.setLayout(new BorderLayout());
		JPanel card2Top = new JPanel();
		card2Top.setLayout(new BoxLayout(card2Top, BoxLayout.Y_AXIS));
		_tipLabel = new JLabel(I18nManager.getText("dialog.correlate.options.tip"));
		card2Top.add(_tipLabel);
		card2Top.add(new JLabel(I18nManager.getText("dialog.correlate.options.intro")));
		// time offset section
		JPanel offsetPanel = new JPanel();
		offsetPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.offsetpanel")));
		offsetPanel.setLayout(new BoxLayout(offsetPanel, BoxLayout.Y_AXIS));
		JPanel offsetPanelTop = new JPanel();
		offsetPanelTop.setLayout(new FlowLayout());
		offsetPanelTop.setBorder(null);
		offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset") + ": "));
		_offsetHourBox = new JTextField(3);
		_offsetHourBox.addKeyListener(optionsChangedListener);
		offsetPanelTop.add(_offsetHourBox);
		offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.hours")));
		_offsetMinBox = new JTextField(3);
		_offsetMinBox.addKeyListener(optionsChangedListener);
		offsetPanelTop.add(_offsetMinBox);
		offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
		_offsetSecBox = new JTextField(3);
		_offsetSecBox.addKeyListener(optionsChangedListener);
		offsetPanelTop.add(_offsetSecBox);
		offsetPanelTop.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
		offsetPanel.add(offsetPanelTop);

		// radio buttons for photo / point later
		JPanel offsetPanelBot = new JPanel();
		offsetPanelBot.setLayout(new FlowLayout());
		offsetPanelBot.setBorder(null);
		_photoLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.photolater"));
		_pointLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.pointlater"));
		_photoLaterOption.addItemListener(optionsChangedListener);
		_pointLaterOption.addItemListener(optionsChangedListener);
		ButtonGroup laterGroup = new ButtonGroup();
		laterGroup.add(_photoLaterOption);
		laterGroup.add(_pointLaterOption);
		offsetPanelBot.add(_photoLaterOption);
		offsetPanelBot.add(_pointLaterOption);
		offsetPanel.add(offsetPanelBot);
		offsetPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
		card2Top.add(offsetPanel);

		// time limits section
		JPanel limitsPanel = new JPanel();
		limitsPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.correlate.options.limitspanel")));
		limitsPanel.setLayout(new BoxLayout(limitsPanel, BoxLayout.Y_AXIS));
		JPanel timeLimitPanel = new JPanel();
		timeLimitPanel.setLayout(new FlowLayout());
		JRadioButton noTimeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.notimelimit"));
		noTimeLimitRadio.addItemListener(optionsChangedListener);
		timeLimitPanel.add(noTimeLimitRadio);
		_timeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.timelimit") + " : ");
		_timeLimitRadio.addItemListener(optionsChangedListener);
		timeLimitPanel.add(_timeLimitRadio);
		groupRadioButtons(noTimeLimitRadio, _timeLimitRadio);
		_limitMinBox = new JTextField(3);
		_limitMinBox.addKeyListener(optionsChangedListener);
		timeLimitPanel.add(_limitMinBox);
		timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.minutes")));
		_limitSecBox = new JTextField(3);
		_limitSecBox.addKeyListener(optionsChangedListener);
		timeLimitPanel.add(_limitSecBox);
		timeLimitPanel.add(new JLabel(I18nManager.getText("dialog.correlate.options.offset.seconds")));
		limitsPanel.add(timeLimitPanel);
		// distance limits
		JPanel distLimitPanel = new JPanel();
		distLimitPanel.setLayout(new FlowLayout());
		JRadioButton noDistLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.nodistancelimit"));
		noDistLimitRadio.addItemListener(optionsChangedListener);
		distLimitPanel.add(noDistLimitRadio);
		_distLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.distancelimit"));
		_distLimitRadio.addItemListener(optionsChangedListener);
		distLimitPanel.add(_distLimitRadio);
		groupRadioButtons(noDistLimitRadio, _distLimitRadio);
		_limitDistBox = new JTextField(4);
		_limitDistBox.addKeyListener(optionsChangedListener);
		distLimitPanel.add(_limitDistBox);
		String[] distUnitsOptions = {I18nManager.getText("units.kilometres"), I18nManager.getText("units.metres"),
			I18nManager.getText("units.miles")};
		_distUnitsDropdown = new JComboBox(distUnitsOptions);
		_distUnitsDropdown.addItemListener(optionsChangedListener);
		distLimitPanel.add(_distUnitsDropdown);
		limitsPanel.add(distLimitPanel);
		limitsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
		card2Top.add(limitsPanel);

		// preview button
		JButton previewButton = new JButton(I18nManager.getText("button.preview"));
		previewButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e)
			{
				createPreview(true);
			}
		});
		card2Top.add(previewButton);
		card2.add(card2Top, BorderLayout.NORTH);
		// preview
		_previewTable = new JTable();
		JScrollPane previewScrollPane = new JScrollPane(_previewTable);
		previewScrollPane.setPreferredSize(new Dimension(300, 100));
		card2.add(previewScrollPane, BorderLayout.CENTER);
		_cards.add(card2, "card2");
		mainPanel.add(_cards, BorderLayout.CENTER);

		// Button panel at the bottom
		JPanel buttonPanel = new JPanel();
		_backButton = new JButton(I18nManager.getText("button.back"));
		_backButton.addActionListener(new ActionListener()
			{
				public void actionPerformed(ActionEvent e)
				{
					CardLayout cl = (CardLayout) _cards.getLayout();
					cl.previous(_cards);
					_backButton.setEnabled(false);
					_nextButton.setEnabled(true);
					_okButton.setEnabled(false);
					_previewEnabled = false;
				}
			});
		_backButton.setEnabled(false);
		buttonPanel.add(_backButton);
		_nextButton = new JButton(I18nManager.getText("button.next"));
		_nextButton.addActionListener(new ActionListener()
			{
				public void actionPerformed(ActionEvent e)
				{
					int rowNum = _photoSelectionTable.getSelectedRow();
					if (rowNum < 0) {rowNum = 0;}
					PhotoSelectionTableRow selectedRow = ((PhotoSelectionTableModel) _photoSelectionTable.getModel())
						.getRow(rowNum);
					setupSecondCard(selectedRow.getTimeDiff());
				}
			});
		buttonPanel.add(_nextButton);
		_okButton = new JButton(I18nManager.getText("button.ok"));
		_okButton.addActionListener(new ActionListener()
			{
				public void actionPerformed(ActionEvent e)
				{
					finishCorrelation();
					_dialog.dispose();
				}
			});
		_okButton.setEnabled(false);
		buttonPanel.add(_okButton);
		JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
		cancelButton.addActionListener(new ActionListener()
			{
				public void actionPerformed(ActionEvent e)
				{
					_dialog.dispose();
				}
			});
		buttonPanel.add(cancelButton);
		mainPanel.add(buttonPanel, BorderLayout.SOUTH);
		return mainPanel;
	}


	/**
	 * Construct a table model for the photo selection table
	 * @param inTrackInfo track info object
	 * @return table model
	 */
	private static PhotoSelectionTableModel makePhotoSelectionTableModel(TrackInfo inTrackInfo)
	{
		PhotoSelectionTableModel model = new PhotoSelectionTableModel();
		int numPhotos = inTrackInfo.getPhotoList().getNumPhotos();
		for (int i=0; i<numPhotos; i++)
		{
			Photo photo = inTrackInfo.getPhotoList().getPhoto(i);
			if (photo.getDataPoint() != null && photo.getDataPoint().hasTimestamp())
			{
				// Calculate time difference, add to table model
				long timeDiff = photo.getTimestamp().getSecondsSince(photo.getDataPoint().getTimestamp());
				model.addPhoto(photo, timeDiff);
			}
		}
		return model;
	}


	/**
	 * Group the two radio buttons together with a ButtonGroup
	 * @param inButton1 first radio button
	 * @param inButton2 second radio button
	 */
	private static void groupRadioButtons(JRadioButton inButton1, JRadioButton inButton2)
	{
		ButtonGroup buttonGroup = new ButtonGroup();
		buttonGroup.add(inButton1);
		buttonGroup.add(inButton2);
		inButton1.setSelected(true);
	}


	/**
	 * Set up the second card using the given time difference and show it
	 * @param inTimeDiff time difference to use for photo time offsets
	 */
	private void setupSecondCard(TimeDifference inTimeDiff)
	{
		_previewEnabled = false;
		boolean hasTimeDiff = inTimeDiff != null;
		if (!hasTimeDiff)
		{
			// No time difference available, so calculate based on computer's time zone
			inTimeDiff = getTimezoneOffset();
		}
		// Use time difference to set edit boxes
		_offsetHourBox.setText("" + inTimeDiff.getNumHours());
		_offsetMinBox.setText("" + inTimeDiff.getNumMinutes());
		_offsetSecBox.setText("" + inTimeDiff.getNumSeconds());
		_photoLaterOption.setSelected(inTimeDiff.getIsPositive());
		_pointLaterOption.setSelected(!inTimeDiff.getIsPositive());
		createPreview(inTimeDiff, true);
		CardLayout cl = (CardLayout) _cards.getLayout();
		cl.last(_cards);
		_backButton.setEnabled(hasTimeDiff);
		_nextButton.setEnabled(false);
		// enable ok button if any photos have been selected
		_okButton.setEnabled(((PhotoPreviewTableModel) _previewTable.getModel()).hasPhotosSelected());
		_previewEnabled = true;
	}


	/**
	 * Create a preview of the correlate action using the selected time difference
	 * @param inFromButton true if triggered from button press, false if automatic
	 */
	public void createPreview(boolean inFromButton)
	{
		// Exit if still on first panel
		if (!_previewEnabled) {return;}
		// Create a TimeDifference based on the edit boxes
		int numHours = getValue(_offsetHourBox.getText());
		int numMins = getValue(_offsetMinBox.getText());
		int numSecs = getValue(_offsetSecBox.getText());
		boolean isPos = _photoLaterOption.isSelected();
		createPreview(new TimeDifference(numHours, numMins, numSecs, isPos), inFromButton);
	}


	/**
	 * Create a preview of the correlate action using the selected time difference
	 * @param inTimeDiff TimeDifference to use for preview
	 * @param inShowWarning true to show warning if all points out of range
	 */
	private void createPreview(TimeDifference inTimeDiff, boolean inShowWarning)
	{
		TimeDifference timeLimit = parseTimeLimit();
		double angDistLimit = parseDistanceLimit();
		PhotoPreviewTableModel model = new PhotoPreviewTableModel();
		PhotoList photos = _app.getTrackInfo().getPhotoList();
		// Loop through photos deciding whether to set correlate flag or not
		int numPhotos = photos.getNumPhotos();
		for (int i=0; i<numPhotos; i++)
		{
			Photo photo = photos.getPhoto(i);
			PointPair pair = getPointPairForPhoto(_app.getTrackInfo().getTrack(), photo, inTimeDiff);
			PhotoPreviewTableRow row = new PhotoPreviewTableRow(pair);
			// Don't try to correlate photos which don't have points either side
			boolean correlatePhoto = pair.isValid();
			// Check time limits, distance limits
			if (timeLimit != null && correlatePhoto) {
				long numSecs = pair.getMinSeconds();
				correlatePhoto = (numSecs <= timeLimit.getTotalSeconds());
			}
			if (angDistLimit > 0.0 && correlatePhoto)
			{
				final double angDistPair = DataPoint.calculateRadiansBetween(pair.getPointBefore(), pair.getPointAfter());
				double frac = pair.getFraction();
				if (frac > 0.5) {frac = 1 - frac;}
				final double angDistPhoto = angDistPair * frac;
				correlatePhoto = (angDistPhoto < angDistLimit);
			}
			// Don't select photos which are already correlated to the same point
			if (pair.getSecondsBefore() == 0L && pair.getPointBefore().getPhoto() != null
				&& pair.getPointBefore().getPhoto().equals(photo)) {
				correlatePhoto = false;
			}
			row.setCorrelateFlag(correlatePhoto);
			model.addPhotoRow(row);
		}
		_previewTable.setModel(model);
		// Set distance units
		model.setDistanceUnits(getSelectedDistanceUnits());
		// Set column widths
		_previewTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
		final int[] colWidths = {150, 160, 100, 100, 50};
		for (int i=0; i<model.getColumnCount(); i++) {
			_previewTable.getColumnModel().getColumn(i).setPreferredWidth(colWidths[i]);
		}
		// check if any photos found
		_okButton.setEnabled(model.hasPhotosSelected());
		if (inShowWarning && !model.hasPhotosSelected())
		{
			JOptionPane.showMessageDialog(_dialog, I18nManager.getText("dialog.correlate.alloutsiderange"),
				I18nManager.getText(getNameKey()), JOptionPane.ERROR_MESSAGE);
		}
	}

	/**
	 * Parse the time limit values entered and validate them
	 * @return TimeDifference object describing limit
	 */
	private TimeDifference parseTimeLimit()
	{
		if (!_timeLimitRadio.isSelected()) {return null;}
		int mins = getValue(_limitMinBox.getText());
		_limitMinBox.setText("" + mins);
		int secs = getValue(_limitSecBox.getText());
		_limitSecBox.setText("" + secs);
		if (mins <= 0 && secs <= 0) {return null;}
		return new TimeDifference(0, mins, secs, true);
	}

	/**
	 * Parse the distance limit value entered and validate
	 * @return angular distance in radians
	 */
	private double parseDistanceLimit()
	{
		double value = -1.0;
		if (_distLimitRadio.isSelected())
		{
			try
			{
				value = Double.parseDouble(_limitDistBox.getText());
			}
			catch (NumberFormatException nfe) {}
		}
		if (value <= 0.0) {
			_limitDistBox.setText("0");
			return -1.0;
		}
		_limitDistBox.setText("" + value);
		return Distance.convertDistanceToRadians(value, getSelectedDistanceUnits());
	}


	/**
	 * @return the selected distance units from the dropdown
	 */
	private Distance.Units getSelectedDistanceUnits()
	{
		final Distance.Units[] distUnits = {Distance.Units.KILOMETRES, Distance.Units.METRES, Distance.Units.MILES};
		return distUnits[_distUnitsDropdown.getSelectedIndex()];
	}


	/**
	 * Try to parse the given string
	 * @param inText String to parse
	 * @return value if parseable, 0 otherwise
	 */
	private static int getValue(String inText)
	{
		int value = 0;
		try {
			value = Integer.parseInt(inText);
		}
		catch (NumberFormatException nfe) {}
		return value;
	}


	/**
	 * Get the point pair surrounding the given photo
	 * @param inTrack track object
	 * @param inPhoto photo object
	 * @param inOffset time offset to apply to photos
	 * @return point pair resulting from correlation
	 */
	private static PointPair getPointPairForPhoto(Track inTrack, Photo inPhoto, TimeDifference inOffset)
	{
		PointPair pair = new PointPair(inPhoto);
		// Add/subtract offet to photo timestamp
		Timestamp photoStamp = inPhoto.getTimestamp().createMinusOffset(inOffset);
		int numPoints = inTrack.getNumPoints();
		for (int i=0; i<numPoints; i++)
		{
			DataPoint point = inTrack.getPoint(i);
			Timestamp pointStamp = point.getTimestamp();
			if (pointStamp != null && pointStamp.isValid())
			{
				long numSeconds = pointStamp.getSecondsSince(photoStamp);
				pair.addPoint(point, numSeconds);
			}
		}
		return pair;
	}


	/**
	 * Construct an array of the point pairs to use for correlation
	 * @return array of PointPair objects
	 */
	private PointPair[] getPointPairs()
	{
		PhotoPreviewTableModel model = (PhotoPreviewTableModel) _previewTable.getModel();
		int numPhotos = model.getRowCount();
		PointPair[] pairs = new PointPair[numPhotos];
		// Loop over photos in preview table model
		for (int i=0; i<numPhotos; i++)
		{
			PhotoPreviewTableRow row = model.getRow(i);
			// add all selected pairs to array (other elements remain null)
			if (row.getCorrelateFlag().booleanValue())
			{
				pairs[i] = row.getPointPair();
			}
		}
		return pairs;
	}

	/**
	 * @return time difference of local time zone from UTC when the first photo was taken
	 */
	private TimeDifference getTimezoneOffset()
	{
		Calendar cal = null;
		// Base time difference on DST when first photo was taken
		Photo firstPhoto = _app.getTrackInfo().getPhotoList().getPhoto(0);
		if (firstPhoto != null && firstPhoto.getTimestamp() != null) {
			cal = firstPhoto.getTimestamp().getCalendar();
		}
		else {
			// No photo or no timestamp, just use current time
			cal = Calendar.getInstance();
		}
		// Both time zone offset and dst offset are based on milliseconds, so convert to seconds
		TimeDifference timeDiff = new TimeDifference((cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 1000);
		return timeDiff;
	}


	/**
	 * Calculate the median index to select from the table
	 * @param inModel table model
	 * @return index of entry to select from table
	 */
	private static int getMedianIndex(PhotoSelectionTableModel inModel)
	{
		// make sortable list
		TreeSet<TimeIndexPair> set = new TreeSet<TimeIndexPair>();
		// loop through rows of table adding to list
		int numRows = inModel.getRowCount();
		int i;
		for (i=0; i<numRows; i++)
		{
			PhotoSelectionTableRow row = inModel.getRow(i);
			set.add(new TimeIndexPair(row.getTimeDiff().getTotalSeconds(), i));
		}
		// pull out middle entry and return index
		TimeIndexPair pair = null;
		Iterator<TimeIndexPair> iterator = set.iterator();
		for (i=0; i<(numRows+1)/2; i++)
		{
			pair = iterator.next();
		}
		return pair.getIndex();
	}


	/**
	 * Disable the ok button
	 */
	public void disableOkButton()
	{
		if (_okButton != null) {
			_okButton.setEnabled(false);
		}
	}


	/**
	 * Check if the track has any uncorrelated photos
	 * @return true if there are any photos which are not connected to points
	 */
	private boolean trackHasUncorrelatedPhotos()
	{
		PhotoList photoList = _app.getTrackInfo().getPhotoList();
		int numPhotos = photoList.getNumPhotos();
		// loop over photos
		for (int i=0; i<numPhotos; i++)
		{
			Photo photo = photoList.getPhoto(i);
			if (photo != null && photo.getDataPoint() == null) {
				return true;
			}
		}
		// no uncorrelated photos found
		return false;
	}

	/**
	 * Finish the correlation by modifying the track
	 * and passing the Undo information back to the App
	 */
	private void finishCorrelation()
	{
		PointPair[] pointPairs = getPointPairs();
		if (pointPairs == null || pointPairs.length <= 0) {return;}

		// begin to construct undo information
		UndoCorrelatePhotos undo = new UndoCorrelatePhotos(_app.getTrackInfo());
		// loop over Photos
		int arraySize = pointPairs.length;
		int i = 0, numPhotos = 0;
		int numPointsToCreate = 0;
		PointPair pair = null;
		for (i=0; i<arraySize; i++)
		{
			pair = pointPairs[i];
			if (pair != null && pair.isValid())
			{
				if (pair.getMinSeconds() == 0L)
				{
					// exact match
					Photo pointPhoto = pair.getPointBefore().getPhoto();
					if (pointPhoto == null)
					{
						// photo coincides with photoless point so connect the two
						pair.getPointBefore().setPhoto(pair.getPhoto());
						pair.getPhoto().setDataPoint(pair.getPointBefore());
					}
					else if (pointPhoto.equals(pair.getPhoto()))
					{
						// photo is already connected, nothing to do
					}
					else
					{
						// point is already connected to a different photo, so need to clone point
						numPointsToCreate++;
					}
				}
				else
				{
					// photo time falls between two points, so need to interpolate new one
					numPointsToCreate++;
				}
				numPhotos++;
			}
		}
		// Second loop, to create points if necessary
		if (numPointsToCreate > 0)
		{
			// make new array for added points
			DataPoint[] addedPoints = new DataPoint[numPointsToCreate];
			int pointNum = 0;
			DataPoint pointToAdd = null;
			for (i=0; i<arraySize; i++)
			{
				pair = pointPairs[i];
				if (pair != null && pair.isValid())
				{
					pointToAdd = null;
					if (pair.getMinSeconds() == 0L && pair.getPointBefore().getPhoto() != null
					 && !pair.getPointBefore().getPhoto().equals(pair.getPhoto()))
					{
						// clone point
						pointToAdd = pair.getPointBefore().clonePoint();
					}
					else if (pair.getMinSeconds() > 0L)
					{
						// interpolate point
						pointToAdd = DataPoint.interpolate(pair.getPointBefore(), pair.getPointAfter(), pair.getFraction());
					}
					if (pointToAdd != null)
					{
						// link photo to point
						pointToAdd.setPhoto(pair.getPhoto());
						pair.getPhoto().setDataPoint(pointToAdd);
						// set to start of segment so not joined in track
						pointToAdd.setSegmentStart(true);
						// add to point array
						addedPoints[pointNum] = pointToAdd;
						pointNum++;
					}
				}
			}
			// expand track
			_app.getTrackInfo().getTrack().appendPoints(addedPoints);
		}

		// send undo information back to controlling app
		undo.setNumPhotosCorrelated(numPhotos);
		_app.completeFunction(undo, ("" + numPhotos + " "
			 + (numPhotos==1?I18nManager.getText("confirm.correlate.single"):I18nManager.getText("confirm.correlate.multi"))));
		// observers already informed by track update
	}
}