package tim.prune.correlate;
import java.awt.BorderLayout;
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.MediaObject;
import tim.prune.data.MediaList;
import tim.prune.data.TimeDifference;
import tim.prune.data.Timestamp;
import tim.prune.data.Track;
import tim.prune.data.Unit;
import tim.prune.data.UnitSetLibrary;
import tim.prune.tips.TipManager;
/**
* Abstract superclass of the two correlator functions
*/
public abstract class Correlator extends GenericFunction
{
protected JDialog _dialog;
private CardStack _cards = null;
private JTable _selectionTable = null;
protected JTable _previewTable = null;
private boolean _previewEnabled = false; // flag required to enable preview function on final panel
private boolean[] _cardEnabled = null; // flag for each card
private JTextField _offsetHourBox = null, _offsetMinBox = null, _offsetSecBox = null;
private JRadioButton _mediaLaterOption = null, _pointLaterOption = null;
private JRadioButton _timeLimitRadio = null, _distLimitRadio = null;
private JTextField _limitMinBox = null, _limitSecBox = null;
private JTextField _limitDistBox = null;
private JComboBox<String> _distUnitsDropdown = null;
private JButton _nextButton = null, _backButton = null;
protected JButton _okButton = null;
/**
* Constructor
* @param inApp App object to report actions to
*/
public Correlator(App inApp) {
super(inApp);
}
/**
* @return type key eg photo, audio
*/
protected abstract String getMediaTypeKey();
/**
* @return media list
*/
protected abstract MediaList getMediaList();
/**
* Begin the function by initialising and showing the dialog
*/
public void begin()
{
// 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;
}
// Show warning if no uncorrelated audios
if (!getMediaList().hasUncorrelatedMedia())
{
Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")};
if (JOptionPane.showOptionDialog(_parentFrame,
I18nManager.getText("dialog.correlate.nouncorrelated" + getMediaTypeKey() + "s"),
I18nManager.getText(getNameKey()), JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
== JOptionPane.NO_OPTION)
{
return;
}
}
// Create dialog if necessary
if (_dialog == null)
{
_dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
_dialog.setLocationRelativeTo(_parentFrame);
_dialog.getContentPane().add(makeDialogContents());
_dialog.pack();
}
// Go to first available card
int card = 0;
_cardEnabled = null;
while (!isCardEnabled(card)) {card++;}
_cards.showCard(card);
showCard(0); // does set up and next/prev enabling
_okButton.setEnabled(false);
if (!isCardEnabled(1)) {
_app.showTip(TipManager.Tip_ManuallyCorrelateOne);
}
_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 CardStack();
// First panel (not required by photo correlator)
JPanel card1 = makeFirstPanel();
if (card1 == null) {card1 = new JPanel();}
_cards.addCard(card1);
// Second panel for selection of linked media
_cards.addCard(makeSecondPanel());
// Third panel for options and preview
_cards.addCard(makeThirdPanel());
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) {
showCard(-1);
}
});
_backButton.setEnabled(false);
buttonPanel.add(_backButton);
_nextButton = new JButton(I18nManager.getText("button.next"));
_nextButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
showCard(1);
}
});
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 / audio selection table
* @return table model
*/
protected MediaSelectionTableModel makeSelectionTableModel()
{
MediaList mediaList = getMediaList();
MediaSelectionTableModel model = new MediaSelectionTableModel(
"dialog.correlate.select." + getMediaTypeKey() + "name",
"dialog.correlate.select." + getMediaTypeKey() + "later");
int numMedia = mediaList.getNumMedia();
for (int i=0; i<numMedia; i++)
{
MediaObject media = mediaList.getMedia(i);
// For working out time differences, can't use media which already had point information
if (media.getDataPoint() != null && media.getDataPoint().hasTimestamp()
&& media.getOriginalStatus() == MediaObject.Status.NOT_CONNECTED)
{
// Calculate time difference, add to table model
long timeDiff = getMediaTimestamp(media).getSecondsSince(media.getDataPoint().getTimestamp());
model.addMedia(media, timeDiff);
}
}
return model;
}
/**
* Group the two radio buttons together with a ButtonGroup
* @param inButton1 first radio button
* @param inButton2 second radio button
*/
protected static void groupRadioButtons(JRadioButton inButton1, JRadioButton inButton2)
{
ButtonGroup buttonGroup = new ButtonGroup();
buttonGroup.add(inButton1);
buttonGroup.add(inButton2);
inButton1.setSelected(true);
}
/**
* Try to parse the given string
* @param inText String to parse
* @return value if parseable, 0 otherwise
*/
protected static int getValue(String inText)
{
int value = 0;
try {
value = Integer.parseInt(inText);
}
catch (NumberFormatException nfe) {}
return value;
}
/**
* @param inFirstTimestamp timestamp of first photo / audio object, or null if not available
* @return time difference of local time zone from UTC when the first photo was taken
*/
private static TimeDifference getTimezoneOffset(Timestamp inFirstTimestamp)
{
Calendar cal = null;
// Use first timestamp if available
if (inFirstTimestamp != null) {
cal = inFirstTimestamp.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
*/
protected static int getMedianIndex(MediaSelectionTableModel 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++)
{
MediaSelectionTableRow 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);
}
}
/**
* @return gui components for first panel, or null if empty
*/
protected JPanel makeFirstPanel() {
return null;
}
/**
* Make the second panel for the selection screen
* @return JPanel object containing gui elements
*/
private JPanel makeSecondPanel()
{
JPanel card = new JPanel();
card.setLayout(new BorderLayout(10, 10));
JLabel introLabel = new JLabel(I18nManager.getText(
"dialog.correlate." + getMediaTypeKey() + "select.intro"));
introLabel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
card.add(introLabel, BorderLayout.NORTH);
// table doesn't have model yet - that will be attached later
_selectionTable = new JTable();
JScrollPane photoScrollPane = new JScrollPane(_selectionTable);
photoScrollPane.setPreferredSize(new Dimension(400, 100));
card.add(photoScrollPane, BorderLayout.CENTER);
return card;
}
/**
* Make contents of third panel including options and preview
* @return JPanel containing gui elements
*/
private JPanel makeThirdPanel()
{
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));
JLabel introLabel = new JLabel(I18nManager.getText("dialog.correlate.options.intro"));
introLabel.setBorder(BorderFactory.createEmptyBorder(8, 6, 5, 6));
card2Top.add(introLabel);
// 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);
_mediaLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options." + getMediaTypeKey() + "later"));
_pointLaterOption = new JRadioButton(I18nManager.getText("dialog.correlate.options.pointlater" + getMediaTypeKey()));
_mediaLaterOption.addItemListener(optionsChangedListener);
_pointLaterOption.addItemListener(optionsChangedListener);
ButtonGroup laterGroup = new ButtonGroup();
laterGroup.add(_mediaLaterOption);
laterGroup.add(_pointLaterOption);
offsetPanelBot.add(_mediaLaterOption);
offsetPanelBot.add(_pointLaterOption);
offsetPanel.add(offsetPanelBot);
offsetPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
card2Top.add(offsetPanel);
// listener for radio buttons
ActionListener radioListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
enableEditBoxes();
}
};
// 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);
noTimeLimitRadio.addActionListener(radioListener);
timeLimitPanel.add(noTimeLimitRadio);
_timeLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.timelimit") + ": ");
_timeLimitRadio.addItemListener(optionsChangedListener);
_timeLimitRadio.addActionListener(radioListener);
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);
noDistLimitRadio.addActionListener(radioListener);
distLimitPanel.add(noDistLimitRadio);
_distLimitRadio = new JRadioButton(I18nManager.getText("dialog.correlate.options.distancelimit") + ": ");
_distLimitRadio.addItemListener(optionsChangedListener);
_distLimitRadio.addActionListener(radioListener);
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<String>(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(new MediaPreviewTableModel("dialog.correlate.select." + getMediaTypeKey() + "name"));
JScrollPane previewScrollPane = new JScrollPane(_previewTable);
previewScrollPane.setPreferredSize(new Dimension(300, 100));
card2.add(previewScrollPane, BorderLayout.CENTER);
return card2;
}
/**
* Go to the next or previous card in the stack
* @param increment 1 for next, -1 for previous card
*/
private void showCard(int increment)
{
int currCard = _cards.getCurrentCardIndex();
int next = currCard + increment;
if (!isCardEnabled(next)) {
next += increment;
}
setupCard(next);
_backButton.setEnabled(next > 0 && (isCardEnabled(next-1) || isCardEnabled(next-2)));
_nextButton.setEnabled(next < (_cards.getNumCards()-1));
_cards.showCard(next);
}
/**
* @param inCardNum index of card
* @return true if specified card is enabled
*/
private boolean isCardEnabled(int inCardNum)
{
if (_cardEnabled == null) {_cardEnabled = getCardEnabledFlags();}
return (inCardNum >= 0 && inCardNum < _cardEnabled.length && _cardEnabled[inCardNum]);
}
/**
* @return array of boolean flags denoting availability of cards
*/
protected boolean[] getCardEnabledFlags()
{
// by default first is off and third is always on; second depends on selection table
return new boolean[] {false, makeSelectionTableModel().getRowCount() > 0, true};
}
/**
* Set up the specified card
* @param inCardNum index of card
*/
protected void setupCard(int inCardNum)
{
_previewEnabled = false;
if (inCardNum == 1)
{
// set up photo selection card
MediaSelectionTableModel model = makeSelectionTableModel();
_selectionTable.setModel(model);
for (int i=0; i<model.getColumnCount(); i++) {
_selectionTable.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);
_selectionTable.getSelectionModel().setSelectionInterval(preselectedIndex, preselectedIndex);
_nextButton.requestFocus();
}
else if (inCardNum == 2)
{
// set up the options/preview card - first check for given time difference
TimeDifference timeDiff = null;
if (isCardEnabled(1))
{
int rowNum = _selectionTable.getSelectedRow();
if (rowNum < 0) {rowNum = 0;}
MediaSelectionTableRow selectedRow =
((MediaSelectionTableModel) _selectionTable.getModel()).getRow(rowNum);
timeDiff = selectedRow.getTimeDiff();
}
setupPreviewCard(timeDiff, getMediaList().getMedia(0));
}
// enable ok button if any photos have been selected
_okButton.setEnabled(inCardNum == 2 && ((MediaPreviewTableModel) _previewTable.getModel()).hasAnySelected());
}
/**
* Enable or disable the edit boxes according to the radio button selections
*/
private void enableEditBoxes()
{
// enable/disable text field for distance input
_limitDistBox.setEnabled(_distLimitRadio.isSelected());
// and for time limits
_limitMinBox.setEnabled(_timeLimitRadio.isSelected());
_limitSecBox.setEnabled(_timeLimitRadio.isSelected());
}
/**
* Parse the time limit values entered and validate them
* @return TimeDifference object describing limit
*/
protected 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
*/
protected 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
*/
protected Unit getSelectedDistanceUnits()
{
final Unit[] distUnits = {UnitSetLibrary.UNITS_KILOMETRES, UnitSetLibrary.UNITS_METRES, UnitSetLibrary.UNITS_MILES};
return distUnits[_distUnitsDropdown.getSelectedIndex()];
}
/**
* 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 = _mediaLaterOption.isSelected();
createPreview(new TimeDifference(numHours, numMins, numSecs, isPos), inFromButton);
}
/**
* Set up the final card using the given time difference and show it
* @param inTimeDiff time difference to use for time offsets
* @param inFirstMedia first media object to use for calculating timezone
*/
protected void setupPreviewCard(TimeDifference inTimeDiff, MediaObject inFirstMedia)
{
_previewEnabled = false;
TimeDifference timeDiff = inTimeDiff;
if (timeDiff == null)
{
// No time difference available, so calculate based on computer's time zone
Timestamp tstamp = null;
if (inFirstMedia != null) {
tstamp = inFirstMedia.getTimestamp();
}
timeDiff = getTimezoneOffset(tstamp);
}
// Use time difference to set edit boxes
_offsetHourBox.setText("" + timeDiff.getNumHours());
_offsetMinBox.setText("" + timeDiff.getNumMinutes());
_offsetSecBox.setText("" + timeDiff.getNumSeconds());
_mediaLaterOption.setSelected(timeDiff.getIsPositive());
_pointLaterOption.setSelected(!timeDiff.getIsPositive());
_previewEnabled = true;
enableEditBoxes();
createPreview(timeDiff, true);
}
/**
* 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
*/
protected abstract void createPreview(TimeDifference inTimeDiff, boolean inShowWarning);
/**
* Get the timestamp of the given media
* @param inMedia media object
* @return normally just returns the media timestamp, overridden by audio correlator
*/
protected Timestamp getMediaTimestamp(MediaObject inMedia)
{
return inMedia.getTimestamp();
}
/**
* Get the point pair surrounding the given media item
* @param inTrack track object
* @param inMedia media object
* @param inOffset time offset to apply
* @return point pair resulting from correlation
*/
protected PointMediaPair getPointPairForMedia(Track inTrack, MediaObject inMedia, TimeDifference inOffset)
{
PointMediaPair pair = new PointMediaPair(inMedia);
if (inMedia.hasTimestamp())
{
// Add/subtract offset to media timestamp
Timestamp mediaStamp = getMediaTimestamp(inMedia).createMinusOffset(inOffset);
int numPoints = inTrack.getNumPoints();
for (int i=0; i<numPoints; i++)
{
DataPoint point = inTrack.getPoint(i);
if (point.getPhoto() == null && point.getAudio() == null)
{
Timestamp pointStamp = point.getTimestamp();
if (pointStamp != null && pointStamp.isValid())
{
long numSeconds = pointStamp.getSecondsSince(mediaStamp);
pair.addPoint(point, numSeconds);
}
}
}
}
return pair;
}
/**
* Finish the correlation
*/
protected abstract void finishCorrelation();
/**
* Construct an array of the point pairs to use for correlation
* @return array of PointMediaPair objects
*/
protected PointMediaPair[] getPointPairs()
{
MediaPreviewTableModel model = (MediaPreviewTableModel) _previewTable.getModel();
int numMedia = model.getRowCount();
PointMediaPair[] pairs = new PointMediaPair[numMedia];
// Loop over items in preview table model
for (int i=0; i<numMedia; i++)
{
MediaPreviewTableRow row = model.getRow(i);
// add all selected pairs to array (other elements remain null)
if (row.getCorrelateFlag().booleanValue()) {
pairs[i] = row.getPointPair();
}
}
return pairs;
}
}