package tim.prune;
import java.util.EmptyStackException;
-import java.util.List;
+import java.util.Set;
import java.util.Stack;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
+import tim.prune.correlate.PhotoCorrelator;
+import tim.prune.correlate.PointPair;
import tim.prune.data.DataPoint;
import tim.prune.data.Field;
import tim.prune.data.Photo;
import tim.prune.gui.UndoManager;
import tim.prune.load.FileLoader;
import tim.prune.load.JpegLoader;
-import tim.prune.load.PhotoMeasurer;
import tim.prune.save.ExifSaver;
import tim.prune.save.FileSaver;
+import tim.prune.save.GpxExporter;
import tim.prune.save.KmlExporter;
import tim.prune.save.PovExporter;
import tim.prune.threedee.ThreeDException;
import tim.prune.threedee.WindowFactory;
import tim.prune.undo.UndoCompress;
import tim.prune.undo.UndoConnectPhoto;
+import tim.prune.undo.UndoCorrelatePhotos;
import tim.prune.undo.UndoDeleteDuplicates;
import tim.prune.undo.UndoDeletePhoto;
import tim.prune.undo.UndoDeletePoint;
import tim.prune.undo.UndoDeleteRange;
+import tim.prune.undo.UndoDisconnectPhoto;
import tim.prune.undo.UndoEditPoint;
import tim.prune.undo.UndoException;
import tim.prune.undo.UndoInsert;
private MenuManager _menuManager = null;
private FileLoader _fileLoader = null;
private JpegLoader _jpegLoader = null;
- private KmlExporter _exporter = null;
+ private FileSaver _fileSaver = null;
+ private KmlExporter _kmlExporter = null;
+ private GpxExporter _gpxExporter = null;
private PovExporter _povExporter = null;
private Stack _undoStack = null;
private UpdateMessageBroker _broker = null;
}
else
{
- FileSaver saver = new FileSaver(this, _frame, _track);
- saver.showDialog(_fileLoader.getLastUsedDelimiter());
+ if (_fileSaver == null) {
+ _fileSaver = new FileSaver(this, _frame, _track);
+ }
+ _fileSaver.showDialog(_fileLoader.getLastUsedDelimiter());
}
}
else
{
// Invoke the export
- if (_exporter == null)
+ if (_kmlExporter == null)
{
- _exporter = new KmlExporter(_frame, _trackInfo);
+ _kmlExporter = new KmlExporter(_frame, _trackInfo);
}
- _exporter.showDialog();
+ _kmlExporter.showDialog();
+ }
+ }
+
+
+ /**
+ * Export track data as Gpx
+ */
+ public void exportGpx()
+ {
+ if (_track == null)
+ {
+ JOptionPane.showMessageDialog(_frame, I18nManager.getText("error.save.nodata"),
+ I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
+ }
+ else
+ {
+ // Invoke the export
+ if (_gpxExporter == null)
+ {
+ _gpxExporter = new GpxExporter(_frame, _trackInfo);
+ }
+ _gpxExporter.showDialog();
}
}
* @param inX X component of unit vector
* @param inY Y component of unit vector
* @param inZ Z component of unit vector
+ * @param inAltitudeCap altitude cap
*/
private void exportPov(boolean inDefineSettings, double inX, double inY, double inZ, int inAltitudeCap)
{
/**
* Complete the point edit
- * @param inEditList list of edits
+ * @param inEditList field values to edit
+ * @param inUndoList field values before edit
*/
public void completePointEdit(FieldEditList inEditList, FieldEditList inUndoList)
{
/**
* Rearrange the waypoints into track order
+ * @param inFunction nearest point, all to end or all to start
*/
public void rearrangeWaypoints(int inFunction)
{
* Receive loaded data and optionally merge with current Track
* @param inFieldArray array of fields
* @param inDataArray array of data
+ * @param inAltFormat altitude format
+ * @param inFilename filename used
*/
public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, int inAltFormat, String inFilename)
{
_undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, photos));
_lastSavePosition = _undoStack.size();
// TODO: Should be possible to reuse the Track object already loaded?
+ _trackInfo.selectPoint(null);
_trackInfo.loadTrack(inFieldArray, inDataArray, inAltFormat);
_trackInfo.getFileInfo().setFile(inFilename);
if (photos != null)
/**
* Accept a list of loaded photos
- * @param inPhotoList List of Photo objects
+ * @param inPhotoSet Set of Photo objects
*/
- public void informPhotosLoaded(List inPhotoList)
+ public void informPhotosLoaded(Set inPhotoSet)
{
- if (inPhotoList != null && !inPhotoList.isEmpty())
+ if (inPhotoSet != null && !inPhotoSet.isEmpty())
{
- int[] numsAdded = _trackInfo.addPhotos(inPhotoList);
+ int[] numsAdded = _trackInfo.addPhotos(inPhotoSet);
int numPhotosAdded = numsAdded[0];
int numPointsAdded = numsAdded[1];
if (numPhotosAdded > 0)
{
// Save numbers so load can be undone
_undoStack.add(new UndoLoadPhotos(numPhotosAdded, numPointsAdded));
- // Trigger preloading of photo sizes in separate thread
- new PhotoMeasurer(_trackInfo.getPhotoList()).measurePhotos();
}
if (numPhotosAdded == 1)
{
_undoStack.add(new UndoConnectPhoto(point, photo.getFile().getName()));
photo.setDataPoint(point);
point.setPhoto(photo);
- //TODO: Confirm connect (maybe with status in photo panel?)
+ _broker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
+ }
+ }
+
+
+ /**
+ * Disconnect the current photo from its point
+ */
+ public void disconnectPhotoFromPoint()
+ {
+ Photo photo = _trackInfo.getCurrentPhoto();
+ if (photo != null && photo.getDataPoint() != null)
+ {
+ DataPoint point = photo.getDataPoint();
+ _undoStack.add(new UndoDisconnectPhoto(point, photo.getFile().getName()));
+ // disconnect
+ photo.setDataPoint(null);
+ point.setPhoto(null);
+ _broker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
}
}
}
+ /**
+ * Begin the photo correlation process by invoking dialog
+ */
+ public void beginCorrelatePhotos()
+ {
+ PhotoCorrelator correlator = new PhotoCorrelator(this, _frame);
+ // TODO: Do we need to keep a reference to this object to reuse it later?
+ correlator.begin();
+ }
+
+
+ /**
+ * Finish the photo correlation process
+ * @param inPointPairs array of PointPair objects describing operation
+ */
+ public void finishCorrelatePhotos(PointPair[] inPointPairs)
+ {
+ // TODO: This method is too big for App, but where should it go?
+ if (inPointPairs != null && inPointPairs.length > 0)
+ {
+ // begin to construct undo information
+ UndoCorrelatePhotos undo = new UndoCorrelatePhotos(_trackInfo);
+ // loop over Photos
+ int arraySize = inPointPairs.length;
+ int i = 0, numPhotos = 0;
+ int numPointsToCreate = 0;
+ PointPair pair = null;
+ for (i=0; i<arraySize; i++)
+ {
+ pair = inPointPairs[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 = inPointPairs[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);
+ // add to point array
+ addedPoints[pointNum] = pointToAdd;
+ pointNum++;
+ }
+ }
+ }
+ // expand track
+ _track.appendPoints(addedPoints);
+ }
+ // add undo information to stack
+ undo.setNumPhotosCorrelated(numPhotos);
+ _undoStack.add(undo);
+ // confirm correlation
+ JOptionPane.showMessageDialog(_frame, "" + numPhotos + " "
+ + (numPhotos==1?I18nManager.getText("dialog.correlate.confirmsingle.text"):I18nManager.getText("dialog.correlate.confirmmultiple.text")),
+ I18nManager.getText("dialog.correlate.title"),
+ JOptionPane.INFORMATION_MESSAGE);
+ // observers already informed by track update
+ }
+ }
+
+
/**
* Save the coordinates of photos in their exif data
*/
}
return num;
}
+
+ /**
+ * Show a brief help message
+ */
+ public void showHelp()
+ {
+ JOptionPane.showMessageDialog(_frame, I18nManager.getText("dialog.help.help"),
+ I18nManager.getText("menu.help"),
+ JOptionPane.INFORMATION_MESSAGE);
+ }
}
/**
* Inform clients that data has been updated
+ * @param inUpdateType type of update
*/
public void dataUpdated(byte inUpdateType);
/**
* Tool to visualize, edit and prune GPS data
+ * Please see the included readme.txt or http://activityworkshop.net
*/
public class GpsPruner
{
- // Final release of version 3
- public static final String VERSION_NUMBER = "3";
- public static final String BUILD_NUMBER = "074";
+ // Final release of version 4
+ public static final String VERSION_NUMBER = "4";
+ public static final String BUILD_NUMBER = "089";
private static App APP = null;
/**
- * Initialize the library
- * using the (optional) locale
+ * Initialize the library using the (optional) locale
+ * @param inLocale locale to use, or null for default
*/
public static void init(Locale inLocale)
{
--- /dev/null
+package tim.prune.correlate;
+
+import java.awt.event.ActionListener;
+import java.awt.event.ActionEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+
+/**
+ * Helper class to listen for changed options on the PhotoCorrelator
+ * Tightly coupled but only to ok button and preview function
+ */
+public class OptionsChangedListener implements KeyListener, ActionListener, ItemListener, Runnable
+{
+ /** Correlator object for callbacks */
+ private PhotoCorrelator _correlator;
+ /** Thread counter */
+ private int _threadCount = 0;
+
+ /** Default delay time from change to preview trigger */
+ private static final long PREVIEW_DELAY_TIME = 2500L;
+
+
+ /**
+ * Constructor
+ * @param inCorrelator correlator object for callbacks
+ */
+ public OptionsChangedListener(PhotoCorrelator inCorrelator)
+ {
+ _correlator = inCorrelator;
+ }
+
+ /**
+ * Respond to actions performed on control
+ * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
+ */
+ public void actionPerformed(ActionEvent inEvent)
+ {
+ optionsChanged();
+ }
+
+ /**
+ * Run method, called by separate thread(s)
+ * @see java.lang.Runnable#run()
+ */
+ public void run()
+ {
+ // Wait for a certain time
+ try {
+ Thread.sleep(PREVIEW_DELAY_TIME);
+ }
+ catch (InterruptedException ie) {}
+ _threadCount--;
+ if (_threadCount == 0) {
+ // trigger preview (false means automatic)
+ _correlator.createPreview(false);
+ }
+ }
+
+ /**
+ * Respond to key pressed event
+ * @param inEvent event
+ */
+ public void keyPressed(KeyEvent inEvent)
+ {
+ optionsChanged();
+ }
+
+ /** Ignore key released events */
+ public void keyReleased(KeyEvent inEvent) {}
+
+ /** Ignore key typed events */
+ public void keyTyped(KeyEvent e) {}
+
+ /**
+ * Respond to item change events (eg dropdown)
+ * @see java.awt.event.ItemListener#itemStateChanged(java.awt.event.ItemEvent)
+ */
+ public void itemStateChanged(ItemEvent inEvent)
+ {
+ if (inEvent.getStateChange() == ItemEvent.SELECTED) {
+ optionsChanged();
+ }
+ }
+
+ /**
+ * Trigger that an option has changed, whatever type
+ */
+ private void optionsChanged()
+ {
+ // disable ok button
+ _correlator.disableOkButton();
+ // start new thread to trigger preview
+ _threadCount++;
+ new Thread(this).start();
+ }
+}
--- /dev/null
+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.JFrame;
+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.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;
+
+/**
+ * Class to manage the automatic correlation of photos to points
+ * including the GUI stuff to control the correlation options
+ */
+public class PhotoCorrelator
+{
+ private App _app;
+ private JFrame _parentFrame;
+ 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
+ * @param inFrame parent frame for dialogs
+ */
+ public PhotoCorrelator(App inApp, JFrame inFrame)
+ {
+ _app = inApp;
+ _parentFrame = inFrame;
+ _dialog = new JDialog(inFrame, I18nManager.getText("dialog.correlate.title"), true);
+ _dialog.setLocationRelativeTo(inFrame);
+ _dialog.getContentPane().add(makeDialogContents());
+ _dialog.pack();
+ }
+
+
+ /**
+ * Reset dialog and show it
+ */
+ 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("dialog.correlate.title"), 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("dialog.correlate.title"), 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.show();
+ }
+
+
+ /**
+ * 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)
+ {
+ _app.finishCorrelatePhotos(getPointPairs());
+ _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.next(_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());
+ //System.out.println("(dist between pair is " + angDistPair + ") which means "
+ // + Distance.convertRadiansToDistance(angDistPair, Distance.UNITS_METRES) + "m");
+ 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("dialog.correlate.title"), 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 int getSelectedDistanceUnits()
+ {
+ final int[] 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 offet to photo timestamp
+ Timestamp photoStamp = inPhoto.getTimestamp().subtractOffset(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 set = new TreeSet();
+ // 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));
+ //System.out.println("pair " + i + " has time " + row.getTimeDiff().getTotalSeconds());
+ }
+ // pull out middle entry and return index
+ TimeIndexPair pair = null;
+ Iterator iterator = set.iterator();
+ for (i=0; i<(numRows+1)/2; i++)
+ {
+ pair = (TimeIndexPair) iterator.next();
+ //System.out.println("After sorting, pair " + i + " has index " + pair.getIndex());
+ }
+ 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;
+ }
+}
--- /dev/null
+package tim.prune.correlate;
+
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import javax.swing.table.AbstractTableModel;
+import tim.prune.I18nManager;
+import tim.prune.data.Distance;
+
+/**
+ * Class to act as table model for the photo preview table
+ */
+public class PhotoPreviewTableModel extends AbstractTableModel
+{
+ /** ArrayList containing TableRow objects */
+ private ArrayList _list = new ArrayList();
+ /** Distance units */
+ private int _distanceUnits = Distance.UNITS_KILOMETRES;
+ /** Number formatter */
+ private static final NumberFormat FORMAT_ONE_DP = NumberFormat.getNumberInstance();
+
+
+ /** Static block to initialise the one d.p. formatter */
+ static
+ {
+ FORMAT_ONE_DP.setMaximumFractionDigits(1);
+ FORMAT_ONE_DP.setMinimumFractionDigits(1);
+ }
+
+
+ /**
+ * @return the column count, always 5
+ */
+ public int getColumnCount()
+ {
+ return 5;
+ }
+
+
+ /**
+ * Get the name of the column
+ * @param inColNum column number
+ * @return column name
+ */
+ public String getColumnName(int inColNum)
+ {
+ if (inColNum == 0) return I18nManager.getText("dialog.correlate.photoselect.photoname");
+ else if (inColNum == 1) return I18nManager.getText("fieldname.timestamp");
+ else if (inColNum == 2) return I18nManager.getText("dialog.correlate.photoselect.timediff");
+ else if (inColNum == 3) return I18nManager.getText("fieldname.distance");
+ return I18nManager.getText("dialog.correlate.options.correlate");
+ }
+
+
+ /**
+ * @return the row count
+ */
+ public int getRowCount()
+ {
+ return _list.size();
+ }
+
+
+ /**
+ * Get the selected row from the table
+ * @param inRowIndex row index
+ * @return table row object
+ */
+ public PhotoPreviewTableRow getRow(int inRowIndex)
+ {
+ PhotoPreviewTableRow row = (PhotoPreviewTableRow) _list.get(inRowIndex);
+ return row;
+ }
+
+
+ /**
+ * Get the value of the specified cell
+ * @param inRowIndex row index
+ * @param inColumnIndex column index
+ * @return value of specified cell
+ */
+ public Object getValueAt(int inRowIndex, int inColumnIndex)
+ {
+ PhotoPreviewTableRow row = (PhotoPreviewTableRow) _list.get(inRowIndex);
+ if (inColumnIndex == 0) return row.getPhoto().getFile().getName();
+ else if (inColumnIndex == 1) {
+ return row.getPhoto().getTimestamp().getText();
+ }
+ else if (inColumnIndex == 2) {
+ if (row.getPointPair().isValid()) {
+ return row.getTimeDiff().getDescription();
+ }
+ return "";
+ }
+ else if (inColumnIndex == 3) {
+ if (row.getPointPair().isValid()) {
+ return FORMAT_ONE_DP.format(row.getDistance(_distanceUnits));
+ }
+ return "";
+ }
+ return row.getCorrelateFlag();
+ }
+
+
+ /**
+ * @param inUnits the distance units to use
+ */
+ public void setDistanceUnits(int inUnits)
+ {
+ _distanceUnits = inUnits;
+ }
+
+
+ /**
+ * Clear the list
+ */
+ public void reset()
+ {
+ _list.clear();
+ }
+
+
+ /**
+ * Add a photo to the list
+ * @param inRow row to add
+ */
+ public void addPhotoRow(PhotoPreviewTableRow inRow)
+ {
+ _list.add(inRow);
+ }
+
+
+ /**
+ * Get the class of objects in the given column
+ * @see javax.swing.table.AbstractTableModel#getColumnClass(int)
+ */
+ public Class getColumnClass(int inColumnIndex)
+ {
+ if (inColumnIndex == 4) {return Boolean.class;}
+ return super.getColumnClass(inColumnIndex);
+ }
+
+
+ /**
+ * Get whether the given cell is editable
+ * @see javax.swing.table.AbstractTableModel#isCellEditable(int, int)
+ */
+ public boolean isCellEditable(int inRowIndex, int inColumnIndex)
+ {
+ if (inColumnIndex == 4) {return true;}
+ return super.isCellEditable(inRowIndex, inColumnIndex);
+ }
+
+
+ /**
+ * @return true if any of the correlate flags are on
+ */
+ public boolean hasPhotosSelected()
+ {
+ for (int i=0; i<getRowCount(); i++)
+ {
+ if (getRow(i).getCorrelateFlag().booleanValue())
+ {
+ return true;
+ }
+ }
+ // None switched on
+ return false;
+ }
+
+
+ /**
+ * Set the value at the given table cell
+ * @see javax.swing.table.AbstractTableModel#setValueAt(java.lang.Object, int, int)
+ */
+ public void setValueAt(Object inValue, int inRowIndex, int inColumnIndex)
+ {
+ // can only edit the correlate column
+ if (inColumnIndex == 4)
+ {
+ PhotoPreviewTableRow row = getRow(inRowIndex);
+ // Don't allow setting of photos which can't be correlated
+ if (row.getPointPair().isValid())
+ {
+ row.setCorrelateFlag(((Boolean) inValue).booleanValue());
+ }
+ }
+ }
+}
--- /dev/null
+package tim.prune.correlate;
+
+import tim.prune.data.Distance;
+
+/**
+ * Class to hold contents of a single row
+ * in the photo preview table
+ */
+public class PhotoPreviewTableRow extends PhotoSelectionTableRow
+{
+ private PointPair _pointPair = null;
+ private double _distance = 0.0;
+ private int _status = 0;
+ private boolean _correlate = false;
+
+
+ /**
+ * Constructor
+ * @param inPointPair point pair object
+ */
+ public PhotoPreviewTableRow(PointPair inPointPair)
+ {
+ super(inPointPair.getPhoto(), inPointPair.getMinSeconds());
+ _pointPair = inPointPair;
+ _distance = inPointPair.getMinRadians();
+ _status = 0;
+ _correlate = (inPointPair.getPhoto().getDataPoint() == null);
+ }
+
+ /**
+ * @param inUnits units to use
+ * @return distance in selected format
+ */
+ public double getDistance(int inUnits)
+ {
+ return Distance.convertRadiansToDistance(_distance, inUnits);
+ }
+
+ /**
+ * @return point status
+ */
+ public int getStatus()
+ {
+ return _status;
+ }
+
+ /**
+ * @return point pair object
+ */
+ public PointPair getPointPair()
+ {
+ return _pointPair;
+ }
+
+ /**
+ * @return flag to set whether to correlate or not
+ */
+ public Boolean getCorrelateFlag()
+ {
+ return Boolean.valueOf(_correlate);
+ }
+
+ /**
+ * @param inFlag true to correlate, false to ignore
+ */
+ public void setCorrelateFlag(boolean inFlag)
+ {
+ _correlate = inFlag;
+ }
+}
--- /dev/null
+package tim.prune.correlate;
+
+import java.util.ArrayList;
+import javax.swing.table.AbstractTableModel;
+
+import tim.prune.I18nManager;
+import tim.prune.data.Photo;
+
+/**
+ * Class to act as table model for the photo selection table
+ */
+public class PhotoSelectionTableModel extends AbstractTableModel
+{
+ private ArrayList _list = new ArrayList();
+
+
+ /**
+ * @return the column count, always 4
+ */
+ public int getColumnCount()
+ {
+ return 4;
+ }
+
+
+ /**
+ * Get the name of the column
+ * @param inColNum column number
+ * @return column name
+ */
+ public String getColumnName(int inColNum)
+ {
+ if (inColNum == 0) return I18nManager.getText("dialog.correlate.photoselect.photoname");
+ else if (inColNum == 1) return I18nManager.getText("fieldname.timestamp");
+ else if (inColNum == 2) return I18nManager.getText("dialog.correlate.photoselect.timediff");
+ return I18nManager.getText("dialog.correlate.photoselect.photolater");
+ }
+
+
+ /**
+ * @return the row count
+ */
+ public int getRowCount()
+ {
+ return _list.size();
+ }
+
+
+ /**
+ * Get the selected row from the table
+ * @param inRowIndex row index
+ * @return table row object
+ */
+ public PhotoSelectionTableRow getRow(int inRowIndex)
+ {
+ PhotoSelectionTableRow row = (PhotoSelectionTableRow) _list.get(inRowIndex);
+ return row;
+ }
+
+
+ /**
+ * Get the value of the specified cell
+ * @param inRowIndex row index
+ * @param inColumnIndex column index
+ * @return value of specified cell
+ */
+ public Object getValueAt(int inRowIndex, int inColumnIndex)
+ {
+ // TODO: only show time of photos (not date) if dates all identical
+ PhotoSelectionTableRow row = (PhotoSelectionTableRow) _list.get(inRowIndex);
+ if (inColumnIndex == 0) return row.getPhoto().getFile().getName();
+ else if (inColumnIndex == 1) return row.getPhoto().getTimestamp().getText();
+ else if (inColumnIndex == 2) return row.getTimeDiff().getDescription();
+ return (row.getTimeDiff().getIsPositive() ? I18nManager.getText("dialog.about.yes") :
+ I18nManager.getText("dialog.about.no"));
+ }
+
+
+ /**
+ * Clear the list
+ */
+ public void reset()
+ {
+ _list.clear();
+ }
+
+ /**
+ * Add a photo to the list
+ * @param inPhoto photo to add
+ * @param inTimeDiff time difference
+ */
+ public void addPhoto(Photo inPhoto, long inTimeDiff)
+ {
+ _list.add(new PhotoSelectionTableRow(inPhoto, inTimeDiff));
+ }
+}
--- /dev/null
+package tim.prune.correlate;
+
+import tim.prune.data.Photo;
+import tim.prune.data.TimeDifference;
+
+/**
+ * Class to hold contents of a single row
+ * in the photo selection table
+ */
+public class PhotoSelectionTableRow
+{
+ private Photo _photo = null;
+ private TimeDifference _timeDiff = null;
+
+ /**
+ * Constructor
+ * @param inPhoto Photo object
+ * @param inNumSecs number of seconds time difference as long
+ */
+ public PhotoSelectionTableRow(Photo inPhoto, long inNumSecs)
+ {
+ _photo = inPhoto;
+ _timeDiff = new TimeDifference(inNumSecs);
+ }
+
+ /**
+ * @return Photo object
+ */
+ public Photo getPhoto()
+ {
+ return _photo;
+ }
+
+ /**
+ * @return time difference
+ */
+ public TimeDifference getTimeDiff()
+ {
+ return _timeDiff;
+ }
+}
--- /dev/null
+package tim.prune.correlate;
+
+import tim.prune.data.DataPoint;
+import tim.prune.data.Photo;
+
+/**
+ * Class to hold a pair of points
+ * used to hold the result of correlation of a photo
+ */
+public class PointPair
+{
+ private Photo _photo = null;
+ private DataPoint _pointBefore = null;
+ private DataPoint _pointAfter = null;
+ private long _secondsBefore = 1L;
+ private long _secondsAfter = -1L;
+
+
+ /**
+ * Constructor
+ * @param inPhoto Photo object
+ */
+ public PointPair(Photo inPhoto)
+ {
+ _photo = inPhoto;
+ }
+
+
+ /**
+ * Add a point to the pair
+ * @param inPoint data point
+ * @param inSeconds number of seconds time difference, positive means point later
+ */
+ public void addPoint(DataPoint inPoint, long inSeconds)
+ {
+ // Check if point is closest point before
+ if (inSeconds <= 0)
+ {
+ // point stamp is before photo stamp
+ if (inSeconds > _secondsBefore || _secondsBefore > 0L)
+ {
+ // point stamp is nearer to photo
+ _pointBefore = inPoint;
+ _secondsBefore = inSeconds;
+ }
+ }
+ // Check if point is closest point after
+ if (inSeconds >= 0)
+ {
+ // point stamp is after photo stamp
+ if (inSeconds < _secondsAfter || _secondsAfter < 0L)
+ {
+ // point stamp is nearer to photo
+ _pointAfter = inPoint;
+ _secondsAfter = inSeconds;
+ }
+ }
+ }
+
+
+ /**
+ * @return Photo object
+ */
+ public Photo getPhoto()
+ {
+ return _photo;
+ }
+
+ /**
+ * @return the closest point before the photo
+ */
+ public DataPoint getPointBefore()
+ {
+ return _pointBefore;
+ }
+
+ /**
+ * @return number of seconds between photo and subsequent point
+ */
+ public long getSecondsBefore()
+ {
+ return _secondsBefore;
+ }
+
+ /**
+ * @return the closest point after the photo
+ */
+ public DataPoint getPointAfter()
+ {
+ return _pointAfter;
+ }
+
+ /**
+ * @return number of seconds between previous point and photo
+ */
+ public long getSecondsAfter()
+ {
+ return _secondsAfter;
+ }
+
+ /**
+ * @return true if both points found
+ */
+ public boolean isValid()
+ {
+ return getPointBefore() != null && getPointAfter() != null;
+ }
+
+ /**
+ * @return the fraction of the distance along the interpolated line
+ */
+ public double getFraction()
+ {
+ if (_secondsAfter == 0L) return 0.0;
+ return (-_secondsBefore * 1.0 / (-_secondsBefore + _secondsAfter));
+ }
+
+ /**
+ * @return the number of seconds to the nearest point
+ */
+ public long getMinSeconds()
+ {
+ return Math.min(_secondsAfter, -_secondsBefore);
+ }
+
+ /**
+ * @return angle from photo to nearest point in radians
+ */
+ public double getMinRadians()
+ {
+ double totalRadians = DataPoint.calculateRadiansBetween(_pointBefore, _pointAfter);
+ double frac = getFraction();
+ return totalRadians * Math.min(frac, 1-frac);
+ }
+}
--- /dev/null
+package tim.prune.correlate;
+
+/**
+ * Simple class to hold a time and an index.
+ * Used in a TreeSet for calculating median time difference
+ */
+public class TimeIndexPair implements Comparable
+{
+ /** Time as long */
+ private long _time = 0L;
+ /** Index as int */
+ private int _index = 0;
+
+
+ /**
+ * Constructor
+ * @param inTime time as long
+ * @param inIndex index as int
+ */
+ public TimeIndexPair(long inTime, int inIndex)
+ {
+ _time = inTime;
+ _index = inIndex;
+ }
+
+
+ /**
+ * @return the index
+ */
+ public int getIndex()
+ {
+ return _index;
+ }
+
+
+ /**
+ * Compare two TimeIndexPair objects
+ * @see java.lang.Comparable#compareTo(java.lang.Object)
+ */
+ public int compareTo(Object inOther)
+ {
+ TimeIndexPair other = (TimeIndexPair) inOther;
+ return (int) (_time - other._time);
+ }
+}
/**
- * Constructor
+ * Constructor using String
+ * @param inString string to parse
+ * @param inFormat format of altitude, either metres or feet
*/
public Altitude(String inString, int inFormat)
{
/**
- * Constructor
+ * Constructor with int vaue
+ * @param inValue int value of altitude
+ * @param inFormat format of altitude, either metres or feet
*/
public Altitude(int inValue, int inFormat)
{
* @return Interpolated Altitude object
*/
public static Altitude interpolate(Altitude inStart, Altitude inEnd, int inIndex, int inNumSteps)
+ {
+ return interpolate(inStart, inEnd, 1.0 * (inIndex + 1) / (inNumSteps + 1));
+ }
+
+
+ /**
+ * Interpolate a new Altitude object between the given ones
+ * @param inStart start altitude
+ * @param inEnd end altitude
+ * @param inFrac fraction of distance from first point
+ * @return Interpolated Altitude object
+ */
+ public static Altitude interpolate(Altitude inStart, Altitude inEnd, double inFrac)
{
// Check if altitudes are valid
if (inStart == null || inEnd == null || !inStart.isValid() || !inEnd.isValid())
int altFormat = inStart.getFormat();
int startValue = inStart.getValue();
int endValue = inEnd.getValue(altFormat);
- int newValue = startValue
- + (int) ((endValue - startValue) * 1.0 / (inNumSteps + 1) * (inIndex + 1));
+ // interpolate between start and end
+ int newValue = startValue + (int) ((endValue - startValue) * inFrac);
return new Altitude(newValue, altFormat);
}
}
/**
* Add a value to the range
- * @param inValue value to add, only positive values considered
+ * @param inAltitude value to add, only positive values considered
*/
public void addValue(Altitude inAltitude)
{
*/
public abstract class Coordinate
{
+ public static final int NO_CARDINAL = -1;
public static final int NORTH = 0;
public static final int EAST = 1;
public static final int SOUTH = 2;
private int _minutes = 0;
private int _seconds = 0;
private int _fracs = 0;
+ private int _fracDenom = 0;
private String _originalString = null;
private int _originalFormat = FORMAT_NONE;
private double _asDouble = 0.0;
}
if (strLen > 1)
{
- // Check for leading character NSEW
- _cardinal = getCardinal(inString.charAt(0));
+ // Check for cardinal character either at beginning or end
+ _cardinal = getCardinal(inString.charAt(0), inString.charAt(strLen-1));
// count numeric fields - 1=d, 2=dm, 3=dm.m/dms, 4=dms.s
int numFields = 0;
boolean inNumeric = false;
char currChar;
- long[] fields = new long[4];
+ long[] fields = new long[4]; // needs to be long for lengthy decimals
long[] denoms = new long[4];
+ String secondDelim = "";
try
{
+ // Loop over characters in input string, populating fields array
for (int i=0; i<strLen; i++)
{
currChar = inString.charAt(i);
else
{
inNumeric = false;
+ // Remember second delimiter
+ if (numFields == 2) {
+ secondDelim += currChar;
+ }
}
}
_valid = (numFields > 0);
// parse fields according to number found
_degrees = (int) fields[0];
_originalFormat = FORMAT_DEG;
+ _fracDenom = 10;
if (numFields == 2)
{
// String is just decimal degrees
_seconds = (int) numSecs;
_fracs = (int) ((numSecs - _seconds) * 10);
}
- else if (numFields == 3)
+ // Differentiate between d-m.f and d-m-s using . or ,
+ else if (numFields == 3 && (secondDelim.equals(".") || secondDelim.equals(",")))
{
// String is degrees-minutes.fractions
_originalFormat = FORMAT_DEG_MIN;
_seconds = (int) numSecs;
_fracs = (int) ((numSecs - _seconds) * 10);
}
- else if (numFields == 4)
+ else if (numFields == 4 || numFields == 3)
{
- _originalFormat = FORMAT_DEG_MIN_SEC;
// String is degrees-minutes-seconds.fractions
+ _originalFormat = FORMAT_DEG_MIN_SEC;
_minutes = (int) fields[1];
_seconds = (int) fields[2];
_fracs = (int) fields[3];
+ _fracDenom = (int) denoms[3];
+ if (_fracDenom < 1) {_fracDenom = 1;}
}
- _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (_seconds / 3600.0) + (_fracs / 36000.0);
+ _asDouble = 1.0 * _degrees + (_minutes / 60.0) + (_seconds / 3600.0) + (_fracs / 3600.0 / _fracDenom);
if (_cardinal == WEST || _cardinal == SOUTH || inString.charAt(0) == '-')
_asDouble = -_asDouble;
+ // validate fields
+ _valid = _valid && (_degrees <= getMaxDegrees() && _minutes < 60 && _seconds < 60 && _fracs < _fracDenom);
}
else _valid = false;
}
+ /**
+ * Get the cardinal from the given character
+ * @param inFirstChar first character from file
+ * @param inLastChar last character from file
+ */
+ protected int getCardinal(char inFirstChar, char inLastChar)
+ {
+ // Try leading character first
+ int cardinal = getCardinal(inFirstChar);
+ // if not there, try trailing character
+ if (cardinal == NO_CARDINAL) {
+ cardinal = getCardinal(inLastChar);
+ }
+ // use default from concrete subclass
+ if (cardinal == NO_CARDINAL) {
+ cardinal = getDefaultCardinal();
+ }
+ return cardinal;
+ }
+
+
/**
* Get the cardinal from the given character
* @param inChar character from file
*/
protected abstract int getCardinal(char inChar);
+ /**
+ * @return the default cardinal for the subclass
+ */
+ protected abstract int getDefaultCardinal();
+
+ /**
+ * @return the maximum degree range for this coordinate
+ */
+ protected abstract int getMaxDegrees();
+
/**
* Constructor
double numSecs = (numMins - _minutes) * 60.0;
_seconds = (int) numSecs;
_fracs = (int) ((numSecs - _seconds) * 10);
+ _fracDenom = 10; // fixed for now
// Make a string to display on screen
_cardinal = inCardinal;
_originalFormat = FORMAT_NONE;
/**
* Output the Coordinate in the given format
- * @param inOriginalString the original String to use as default
* @param inFormat format to use, eg FORMAT_DEG_MIN_SEC
* @return String for output
*/
.append(threeDigitString(_degrees)).append('°')
.append(twoDigitString(_minutes)).append('\'')
.append(twoDigitString(_seconds)).append('.')
- .append(_fracs);
+ .append(formatFraction(_fracs, _fracDenom));
answer = buffer.toString();
break;
}
case FORMAT_DEG_MIN:
{
answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°"
- + (_minutes + _seconds / 60.0 + _fracs / 600.0) + "'";
+ + (_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom) + "'";
break;
}
case FORMAT_DEG_WHOLE_MIN:
{
answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°"
- + (int) Math.floor(_minutes + _seconds / 60.0 + _fracs / 600.0 + 0.5) + "'";
+ + (int) Math.floor(_minutes + _seconds / 60.0 + _fracs / 60.0 / _fracDenom + 0.5) + "'";
break;
}
case FORMAT_DEG:
case FORMAT_DEG_WITHOUT_CARDINAL:
{
answer = (_asDouble<0.0?"-":"")
- + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 36000.0);
+ + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 3600.0 / _fracDenom);
break;
}
case FORMAT_DEG_MIN_SEC_WITH_SPACES:
{
- answer = "" + _degrees + " " + _minutes + " " + _seconds + "." + _fracs;
+ // Note: cardinal not needed as this format is only for exif, which has cardinal separately
+ answer = "" + _degrees + " " + _minutes + " " + _seconds + "." + formatFraction(_fracs, _fracDenom);
break;
}
case FORMAT_CARDINAL:
return answer;
}
+ /**
+ * Format the fraction part of seconds value
+ * @param inFrac fractional part eg 123
+ * @param inDenom denominator of fraction eg 10000
+ * @return String describing fraction, in this case 0123
+ */
+ private static final String formatFraction(int inFrac, int inDenom)
+ {
+ if (inDenom <= 1 || inFrac == 0) {return "" + inFrac;}
+ String denomString = "" + inDenom;
+ int reqdLen = denomString.length() - 1;
+ String result = denomString + inFrac;
+ int resultLen = result.length();
+ return result.substring(resultLen - reqdLen);
+ }
+
/**
* Format an integer to a two-digit String
*/
public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
int inIndex, int inNumPoints)
+ {
+ return interpolate(inStart, inEnd, 1.0 * (inIndex+1) / (inNumPoints + 1));
+ }
+
+
+ /**
+ * Create a new Coordinate between two others
+ * @param inStart start coordinate
+ * @param inEnd end coordinate
+ * @param inFraction fraction from start to end
+ * @return new Coordinate object
+ */
+ public static Coordinate interpolate(Coordinate inStart, Coordinate inEnd,
+ double inFraction)
{
double startValue = inStart.getDouble();
double endValue = inEnd.getDouble();
- double newValue = startValue + (endValue - startValue) * (inIndex+1) / (inNumPoints + 1);
+ double newValue = startValue + (endValue - startValue) * inFraction;
Coordinate answer = inStart.makeNew(newValue, inStart._originalFormat);
return answer;
}
/**
* Create a String representation for debug
+ * @return String describing coordinate value
*/
public String toString()
{
- return "Coord: " + _cardinal + " (" + _degrees + ") (" + _minutes + ") (" + _seconds + "." + _fracs + ") = " + _asDouble;
+ return "Coord: " + _cardinal + " (" + _degrees + ") (" + _minutes + ") (" + _seconds + "."
+ + formatFraction(_fracs, _fracDenom) + ") = " + _asDouble;
}
}
}
+ /** @return latitude */
public Coordinate getLatitude()
{
return _latitude;
}
+ /** @return longitude */
public Coordinate getLongitude()
{
return _longitude;
}
+ /** @return true if point has altitude */
public boolean hasAltitude()
{
return _altitude.isValid();
}
+ /** @return altitude */
public Altitude getAltitude()
{
return _altitude;
}
+ /** @return true if point has timestamp */
public boolean hasTimestamp()
{
return _timestamp.isValid();
}
+ /** @return timestamp */
public Timestamp getTimestamp()
{
return _timestamp;
}
+ /** @return waypoint name, if any */
public String getWaypointName()
{
return _waypointName;
{
return !inOther.isWaypoint();
}
- else
- {
- return (inOther._waypointName != null && inOther._waypointName.equals(_waypointName));
- }
+ return (inOther._waypointName != null && inOther._waypointName.equals(_waypointName));
}
public DataPoint[] interpolate(DataPoint inEndPoint, int inNumPoints)
{
DataPoint[] range = new DataPoint[inNumPoints];
- Coordinate endLatitude = inEndPoint.getLatitude();
- Coordinate endLongitude = inEndPoint.getLongitude();
- Altitude endAltitude = inEndPoint.getAltitude();
-
// Loop over points
for (int i=0; i<inNumPoints; i++)
{
- Coordinate latitude = Coordinate.interpolate(_latitude, endLatitude, i, inNumPoints);
- Coordinate longitude = Coordinate.interpolate(_longitude, endLongitude, i, inNumPoints);
- Altitude altitude = Altitude.interpolate(_altitude, endAltitude, i, inNumPoints);
+ Coordinate latitude = Coordinate.interpolate(_latitude, inEndPoint.getLatitude(), i, inNumPoints);
+ Coordinate longitude = Coordinate.interpolate(_longitude, inEndPoint.getLongitude(), i, inNumPoints);
+ Altitude altitude = Altitude.interpolate(_altitude, inEndPoint.getAltitude(), i, inNumPoints);
range[i] = new DataPoint(latitude, longitude, altitude);
}
return range;
}
+ /**
+ * Interpolate between the two given points
+ * @param inStartPoint start point
+ * @param inEndPoint end point
+ * @param inFrac fractional distance from first point (0.0 to 1.0)
+ * @return new DataPoint object between two given ones
+ */
+ public static DataPoint interpolate(DataPoint inStartPoint, DataPoint inEndPoint, double inFrac)
+ {
+ if (inStartPoint == null || inEndPoint == null) {return null;}
+ return new DataPoint(
+ Coordinate.interpolate(inStartPoint.getLatitude(), inEndPoint.getLatitude(), inFrac),
+ Coordinate.interpolate(inStartPoint.getLongitude(), inEndPoint.getLongitude(), inFrac),
+ Altitude.interpolate(inStartPoint.getAltitude(), inEndPoint.getAltitude(), inFrac)
+ );
+ }
/**
* Calculate the number of radians between two points (for distance calculation)
}
return false;
}
+
+
+ /**
+ * Get string for debug
+ * @see java.lang.Object#toString()
+ */
+ public String toString()
+ {
+ return "[Lat=" + getLatitude().toString() + ", Lon=" + getLongitude().toString() + "]";
+ }
}
// distance formats
public static final int UNITS_KILOMETRES = 1;
public static final int UNITS_MILES = 2;
+ public static final int UNITS_METRES = 3;
// Geographical constants
private static final double EARTH_RADIUS_KM = 6372.795;
- private static final double EARTH_RADIUS_MILES = 3959.8712255;
// Conversion constants
- //private static final double CONVERT_KM_TO_MILES = 1.609344;
- //private static final double CONVERT_MILES_TO_KM = 0.621371192;
+ private static final double CONVERT_KM_TO_MILES = 0.621371192;
/**
* Convert the given angle in radians into a distance
* @param inAngDist angular distance in radians
- * @param inUnits desired units, miles or km
+ * @param inUnits desired units, eg miles or km
* @return distance in specified format
*/
- public static double convertRadians(double inAngDist, int inUnits)
+ public static double convertRadiansToDistance(double inAngDist, int inUnits)
{
// Multiply by appropriate factor
if (inUnits == UNITS_MILES)
- return inAngDist * EARTH_RADIUS_MILES;
+ return inAngDist * EARTH_RADIUS_KM * CONVERT_KM_TO_MILES;
+ else if (inUnits == UNITS_METRES)
+ return inAngDist * EARTH_RADIUS_KM * 1000;
+ // default kilometres
return inAngDist * EARTH_RADIUS_KM;
}
+ /**
+ * Convert the given distance into an angle in radians
+ * @param inDist distance to convert
+ * @param inUnits units, eg miles or km
+ * @return angular distance in radians
+ */
+ public static double convertDistanceToRadians(double inDist, int inUnits)
+ {
+ // Divide by appropriate factor
+ if (inUnits == UNITS_MILES)
+ return inDist / EARTH_RADIUS_KM / CONVERT_KM_TO_MILES;
+ else if (inUnits == UNITS_METRES)
+ return inDist / EARTH_RADIUS_KM / 1000;
+ // default kilometres
+ return inDist / EARTH_RADIUS_KM;
+ }
}
return _builtin;
}
+ /**
+ * @return field type
+ */
+ public FieldType getType()
+ {
+ return _type;
+ }
+
/**
* Checks if the two fields are equal
* @param inOther other Field object
/**
* Constructor giving array of Field objects
- * @param inFieldArray
+ * @param inFieldArray array of Field objects
*/
public FieldList(Field[] inFieldArray)
{
*/
public class FieldType
{
- private int _id = 0;
+ private char _id = 0;
- public static final FieldType NONE = new FieldType(0);
- public static final FieldType INT = new FieldType(1);
- public static final FieldType BOOL = new FieldType(2);
- public static final FieldType COORD = new FieldType(3);
- public static final FieldType TIME = new FieldType(4);
+ public static final FieldType NONE = new FieldType('0');
+ public static final FieldType INT = new FieldType('1');
+ public static final FieldType BOOL = new FieldType('2');
+ public static final FieldType COORD = new FieldType('3');
+ public static final FieldType TIME = new FieldType('4');
/**
* Private constructor
* @param inId identifier
*/
- private FieldType(int inId)
+ private FieldType(char inId)
{
_id = inId;
}
protected int getCardinal(char inChar)
{
// Latitude recognises N, S and -
- // default is North
- int cardinal = NORTH;
+ // default is No cardinal
+ int cardinal = NO_CARDINAL;
switch (inChar)
{
case 'N':
return cardinal;
}
+ /**
+ * @return default cardinal (North)
+ * @see tim.prune.data.Coordinate#getDefaultCardinal()
+ */
+ protected int getDefaultCardinal()
+ {
+ return NORTH;
+ }
/**
* Make a new Latitude object
return new Latitude(inValue, inFormat);
}
+ /**
+ * @return the maximum degree range for this coordinate
+ */
+ protected int getMaxDegrees()
+ {
+ return 90;
+ }
}
protected int getCardinal(char inChar)
{
// Longitude recognises E, W and -
- // default is East
- int cardinal = EAST;
+ // default is no cardinal
+ int cardinal = NO_CARDINAL;
switch (inChar)
{
case 'E':
}
+ /**
+ * @return default cardinal (East)
+ * @see tim.prune.data.Coordinate#getDefaultCardinal()
+ */
+ protected int getDefaultCardinal()
+ {
+ return EAST;
+ }
+
+
/**
* Make a new Longitude object
* @see tim.prune.data.Coordinate#makeNew(double, int)
return new Longitude(inValue, inFormat);
}
+ /**
+ * @return the maximum degree range for this coordinate
+ */
+ protected int getMaxDegrees()
+ {
+ return 180;
+ }
}
/** Current photo status */
private byte _currentStatus = PhotoStatus.NOT_CONNECTED;
// TODO: Need to store caption for image?
- // TODO: Need to store thumbnail for image?
+ // thumbnail for image (from exif)
+ private byte[] _exifThumbnail = null;
/**
_currentStatus = inStatus;
}
+ /**
+ * @return byte array of thumbnail data
+ */
+ public byte[] getExifThumbnail()
+ {
+ return _exifThumbnail;
+ }
+
+ /**
+ * @param inBytes byte array from exif
+ */
+ public void setExifThumbnail(byte[] inBytes)
+ {
+ _exifThumbnail = inBytes;
+ }
/**
* Delete the cached data when the Photo is no longer needed
package tim.prune.data;
+import tim.prune.DataSubscriber;
import tim.prune.UpdateMessageBroker;
/**
/**
- * @param inFormat distance units to use, from class Distance
+ * @param inUnits distance units to use, from class Distance
* @return distance of Selection in specified units
*/
public double getDistance(int inUnits)
{
- return Distance.convertRadians(_angDistance, inUnits);
+ return Distance.convertRadiansToDistance(_angDistance, inUnits);
}
*/
public int getCurrentPhotoIndex()
{
- // System.out.println("Current photo index = " + _currentPhotoIndex);
return _currentPhotoIndex;
}
_currentPoint = _startIndex = _endIndex = -1;
}
}
- _broker.informSubscribers();
+ _broker.informSubscribers(DataSubscriber.SELECTION_CHANGED);
}
}
--- /dev/null
+package tim.prune.data;
+
+import tim.prune.I18nManager;
+
+/**
+ * Class to represent a time difference, like the difference between two Timestamp objects,
+ * and methods for representing and displaying them.
+ */
+public class TimeDifference
+{
+ private long _totalSeconds = 0L;
+ private int _seconds = 0;
+ private int _minutes = 0;
+ private int _hours = 0;
+ private String _description = null;
+
+
+ /**
+ * Constructor using long
+ * @param inNumSeconds number of seconds time difference
+ */
+ public TimeDifference(long inNumSeconds)
+ {
+ _totalSeconds = inNumSeconds;
+ if (inNumSeconds < 0) {inNumSeconds = -inNumSeconds;}
+ _hours = (int) (inNumSeconds / 60 / 60);
+ _minutes = (int) (inNumSeconds / 60 - _hours * 60);
+ _seconds = (int) (inNumSeconds % 60);
+ }
+
+
+ /**
+ * Constructor giving each field separately
+ * @param inHours number of hours
+ * @param inMinutes number of minutes
+ * @param inSeconds number of seconds
+ * @param inPositive true for positive time difference
+ */
+ public TimeDifference(int inHours, int inMinutes, int inSeconds, boolean inPositive)
+ {
+ // Check for negative values?
+ _hours = inHours;
+ _minutes = inMinutes;
+ _seconds = inSeconds;
+ _totalSeconds = inHours * 3600L + inMinutes * 60L + inSeconds;
+ if (!inPositive) {_totalSeconds = -_totalSeconds;}
+ }
+
+
+ /**
+ * @return total number of seconds time difference
+ */
+ public long getTotalSeconds()
+ {
+ return _totalSeconds;
+ }
+
+ /**
+ * @return number of hours
+ */
+ public int getNumHours()
+ {
+ return _hours;
+ }
+
+ /**
+ * @return number of minutes
+ */
+ public int getNumMinutes()
+ {
+ return _minutes;
+ }
+
+ /**
+ * @return number of seconds
+ */
+ public int getNumSeconds()
+ {
+ return _seconds;
+ }
+
+ /**
+ * @return true if time difference positive
+ */
+ public boolean getIsPositive()
+ {
+ return _totalSeconds >= 0L;
+ }
+
+
+ /**
+ * Build a String to describe the time duration
+ * @return time as a string, days, hours, mins, secs as appropriate
+ */
+ public String getDescription()
+ {
+ if (_description != null) {return _description;}
+ StringBuffer buffer = new StringBuffer();
+ boolean started = false;
+ // hours
+ if (_hours > 0)
+ {
+ buffer.append(_hours).append(' ').append(I18nManager.getText("display.range.time.hours"));
+ started = true;
+ }
+ // minutes
+ if (_minutes > 0)
+ {
+ if (started) {buffer.append(", ");}
+ else {started = true;}
+ buffer.append(_minutes).append(' ').append(I18nManager.getText("display.range.time.mins"));
+ }
+ // seconds
+ if (_seconds > 0 || !started)
+ {
+ if (started) {buffer.append(", ");}
+ buffer.append(_seconds).append(' ').append(I18nManager.getText("display.range.time.secs"));
+ }
+ _description = buffer.toString();
+ return _description;
+ }
+
+}
private boolean _valid = false;
private long _seconds = 0L;
private String _text = null;
+ private String _timeText = null;
private static DateFormat DEFAULT_DATE_FORMAT = DateFormat.getDateTimeInstance();
+ private static DateFormat DEFAULT_TIME_FORMAT = DateFormat.getTimeInstance();
+ private static DateFormat ISO_8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
private static DateFormat[] ALL_DATE_FORMATS = null;
private static Calendar CALENDAR = null;
private static long SECS_SINCE_1970 = 0L;
private static long MSECS_SINCE_1970 = 0L;
private static long MSECS_SINCE_1990 = 0L;
private static long TWENTY_YEARS_IN_SECS = 0L;
-
private static final long GARTRIP_OFFSET = 631065600L;
+ /** Specifies original timestamp format */
+ public static final int FORMAT_ORIGINAL = 0;
+ /** Specifies locale-dependent timestamp format */
+ public static final int FORMAT_LOCALE = 1;
+ /** Specifies ISO 8601 timestamp format */
+ public static final int FORMAT_ISO_8601 = 2;
+
// Static block to initialise offsets
static
{
new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy"),
new SimpleDateFormat("HH:mm:ss dd MMM yyyy"),
new SimpleDateFormat("dd MMM yyyy HH:mm:ss"),
- new SimpleDateFormat("yyyy MMM dd HH:mm:ss")
+ new SimpleDateFormat("yyyy MMM dd HH:mm:ss"),
+ ISO_8601_FORMAT
};
}
/**
* Constructor
+ * @param inString String containing timestamp
*/
public Timestamp(String inString)
{
_seconds = rawValue / 1000L + TWENTY_YEARS_IN_SECS;
smallestDiff = diff3;
}
- // Lastly, check garmin offset
+ // Lastly, check gartrip offset
if (diff4 < smallestDiff)
{
- // seconds since garmin offset
+ // seconds since gartrip offset
_seconds = rawValue + GARTRIP_OFFSET;
}
_valid = true;
/**
- * Constructor giving millis since 1970
- * @param inMillis
+ * Constructor giving millis
+ * @param inMillis milliseconds since 1970
*/
public Timestamp(long inMillis)
{
}
+ /**
+ * Add the given TimeDifference to this Timestamp
+ * @param inOffset TimeDifference to add
+ * @return new Timestamp object
+ */
+ public Timestamp addOffset(TimeDifference inOffset)
+ {
+ return new Timestamp((_seconds + inOffset.getTotalSeconds()) * 1000L);
+ }
+
+
+ /**
+ * Subtract the given TimeDifference from this Timestamp
+ * @param inOffset TimeDifference to subtract
+ * @return new Timestamp object
+ */
+ public Timestamp subtractOffset(TimeDifference inOffset)
+ {
+ return new Timestamp((_seconds - inOffset.getTotalSeconds()) * 1000L);
+ }
+
+
/**
* @return Description of timestamp in locale-specific format
*/
public String getText()
{
+ return getText(FORMAT_LOCALE);
+ }
+
+ /**
+ * @param inFormat format of timestamp
+ * @return Description of timestamp in required format
+ */
+ public String getText(int inFormat)
+ {
+ if (inFormat == FORMAT_ISO_8601) {
+ return format(ISO_8601_FORMAT);
+ }
if (_text == null)
{
- if (_valid)
- {
- CALENDAR.setTimeInMillis(_seconds * 1000L);
- _text = DEFAULT_DATE_FORMAT.format(CALENDAR.getTime());
+ if (_valid) {
+ _text = format(DEFAULT_DATE_FORMAT);
}
else _text = "";
}
return _text;
}
+
+ /**
+ * @return Description of time part of timestamp in locale-specific format
+ */
+ public String getTimeText()
+ {
+ if (_timeText == null)
+ {
+ if (_valid) {
+ _timeText = format(DEFAULT_TIME_FORMAT);
+ }
+ else _timeText = "";
+ }
+ return _timeText;
+ }
+
+ /**
+ * Utility method for formatting dates / times
+ * @param inFormat formatter object
+ * @return formatted String
+ */
+ private String format(DateFormat inFormat)
+ {
+ CALENDAR.setTimeInMillis(_seconds * 1000L);
+ return inFormat.format(CALENDAR.getTime());
+ }
+
+ /**
+ * @return a Calendar object representing this timestamp
+ */
+ public Calendar getCalendar()
+ {
+ Calendar cal = Calendar.getInstance();
+ cal.setTimeInMillis(_seconds * 1000L);
+ return cal;
+ }
}
/**
- * Constructor giving arrays of Fields and Objects
- * @param inFieldArray field array
- * @param inPointArray 2d array of field values
+ * Constructor for empty track
+ * @param inBroker message broker object
*/
public Track(UpdateMessageBroker inBroker)
{
System.arraycopy(newPointArray, 0, _dataPoints, 0, numCopied);
_numPoints = _dataPoints.length;
_scaled = false;
- _broker.informSubscribers();
}
return numDeleted;
}
/**
* Delete the specified point
+ * @param inIndex point index
* @return true if successful
*/
public boolean deletePoint(int inIndex)
_dataPoints = newPointArray;
_numPoints = _dataPoints.length;
_scaled = false;
- _broker.informSubscribers();
}
return numDupes;
}
/**
* Reverse the specified range of points
+ * @param inStart start index
+ * @param inEnd end index
* @return true if successful, false otherwise
*/
public boolean reverseRange(int inStart, int inEnd)
/**
* Replace the track contents with the given point array
* @param inContents array of DataPoint objects
+ * @return true on success
*/
public boolean replaceContents(DataPoint[] inContents)
{
package tim.prune.data;
-import java.util.List;
-
+import java.util.Iterator;
+import java.util.Set;
import tim.prune.UpdateMessageBroker;
/**
/**
- * Add a List of Photos
- * @param inList List containing Photo objects
+ * Add a Set of Photos
+ * @param inSet Set containing Photo objects
* @return array containing number of photos and number of points added
*/
- public int[] addPhotos(List inList)
+ public int[] addPhotos(Set inSet)
{
- // TODO: Should photos be sorted at load-time, either by filename or date?
// Firstly count number of points and photos to add
int numPhotosToAdd = 0;
int numPointsToAdd = 0;
- if (inList != null && !inList.isEmpty())
+ Iterator iterator = null;
+ if (inSet != null && !inSet.isEmpty())
{
- for (int i=0; i<inList.size(); i++)
+ iterator = inSet.iterator();
+ while (iterator.hasNext())
{
try
{
- Photo photo = (Photo) inList.get(i);
+ Photo photo = (Photo) iterator.next();
if (photo != null && !_photoList.contains(photo))
{
numPhotosToAdd++;
int pointNum = 0;
boolean hasAltitude = false;
// Add each Photo in turn
- for (int i=0; i<inList.size(); i++)
+ iterator = inSet.iterator();
+ while (iterator.hasNext())
{
try
{
- Photo photo = (Photo) inList.get(i);
+ Photo photo = (Photo) iterator.next();
if (photo != null && !_photoList.contains(photo))
{
// Add photo
public int compress(int inResolution)
{
int numDeleted = _track.compress(inResolution);
- if (numDeleted > 0)
+ if (numDeleted > 0) {
_selection.clearAll();
+ _broker.informSubscribers();
+ }
return numDeleted;
}
public int deleteDuplicates()
{
int numDeleted = _track.deleteDuplicates();
- if (numDeleted > 0)
+ if (numDeleted > 0) {
_selection.clearAll();
+ _broker.informSubscribers();
+ }
return numDeleted;
}
*/\r
private boolean _isMotorolaByteOrder;\r
\r
+ /** Thumbnail offset */\r
+ private int _thumbnailOffset = -1;\r
+ /** Thumbnail length */\r
+ private int _thumbnailLength = -1;\r
+\r
/**\r
* The number of bytes used per format descriptor.\r
*/\r
public static final int TAG_GPS_TIMESTAMP = 0x0007;\r
/** GPS date (atomic clock) GPSDateStamp 23 1d RATIONAL 3 */\r
public static final int TAG_GPS_DATESTAMP = 0x001d;\r
+ /** Exif timestamp */\r
+ public static final int TAG_DATETIME_ORIGINAL = 0x9003;\r
+ /** Thumbnail offset */\r
+ private static final int TAG_THUMBNAIL_OFFSET = 0x0201;\r
+ /** Thumbnail length */\r
+ private static final int TAG_THUMBNAIL_LENGTH = 0x0202;\r
+\r
\r
/**\r
* Creates an ExifReader for a Jpeg file.\r
* @param inFile File object to attempt to read from\r
- * @throws JpegProcessingException on failure\r
+ * @throws JpegException on failure\r
*/\r
public ExifReader(File inFile) throws JpegException\r
{\r
// Calculate the value as an offset for cases where the tag represents a directory\r
final int subdirOffset = inTiffHeaderOffset + get32Bits(tagValueOffset);\r
\r
- // TODO: Also look for timestamp(s) in Exif for correlation - which directory?\r
+ // Look in both basic Exif tags (for timestamp, thumbnail) and Gps tags (for lat, long, altitude, timestamp)\r
switch (tagType)\r
{\r
case TAG_EXIF_OFFSET:\r
- // ignore\r
+ processDirectory(inMetadata, false, inDirectoryOffsets, subdirOffset, inTiffHeaderOffset);\r
continue;\r
case TAG_INTEROP_OFFSET:\r
// ignore\r
continue;\r
default:\r
// not a known directory, so must just be a normal tag\r
- // ignore if we're not in gps directory\r
if (inIsGPS)\r
+ {\r
processGpsTag(inMetadata, tagType, tagValueOffset, componentCount, formatCode);\r
+ }\r
+ else\r
+ {\r
+ processExifTag(inMetadata, tagType, tagValueOffset, componentCount, formatCode);\r
+ }\r
break;\r
}\r
}\r
inMetadata.setAltitude(readRational(inTagValueOffset, inFormatCode, inComponentCount));\r
break;\r
case TAG_GPS_TIMESTAMP:\r
- inMetadata.setTimestamp(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount));\r
+ inMetadata.setGpsTimestamp(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount));\r
break;\r
case TAG_GPS_DATESTAMP:\r
- inMetadata.setDatestamp(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount));\r
+ inMetadata.setGpsDatestamp(readRationalArray(inTagValueOffset, inFormatCode, inComponentCount));\r
break;\r
default: // ignore all other tags\r
}\r
}\r
\r
\r
+ /**\r
+ * Process a general Exif tag\r
+ * @param inMetadata metadata holding extracted values\r
+ * @param inTagType tag type (eg latitude)\r
+ * @param inTagValueOffset start offset in data array\r
+ * @param inComponentCount component count for tag\r
+ * @param inFormatCode format code, eg byte\r
+ */\r
+ private void processExifTag(JpegData inMetadata, int inTagType, int inTagValueOffset,\r
+ int inComponentCount, int inFormatCode)\r
+ {\r
+ // Only interested in original timestamp, thumbnail offset and thumbnail length\r
+ if (inTagType == TAG_DATETIME_ORIGINAL)\r
+ {\r
+ inMetadata.setOriginalTimestamp(readString(inTagValueOffset, inFormatCode, inComponentCount));\r
+ }\r
+ else if (inTagType == TAG_THUMBNAIL_OFFSET) {\r
+ _thumbnailOffset = TIFF_HEADER_START_OFFSET + get16Bits(inTagValueOffset);\r
+ extractThumbnail(inMetadata);\r
+ }\r
+ else if (inTagType == TAG_THUMBNAIL_LENGTH) {\r
+ _thumbnailLength = get16Bits(inTagValueOffset);\r
+ extractThumbnail(inMetadata);\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Attempt to extract the thumbnail image\r
+ */\r
+ private void extractThumbnail(JpegData inMetadata)\r
+ {\r
+ if (_thumbnailOffset > 0 && _thumbnailLength > 0 && inMetadata.getThumbnailImage() == null)\r
+ {\r
+ byte[] thumbnailBytes = new byte[_thumbnailLength];\r
+ System.arraycopy(_data, _thumbnailOffset, thumbnailBytes, 0, _thumbnailLength);\r
+ inMetadata.setThumbnailImage(thumbnailBytes);\r
+ }\r
+ }\r
+\r
+\r
/**\r
* Calculate the tag value offset\r
* @param inByteCount\r
private Rational[] _latitude = null;
private Rational[] _longitude = null;
private Rational _altitude = null;
- private Rational[] _timestamp = null;
- private Rational[] _datestamp = null;
+ private Rational[] _gpsTimestamp = null;
+ private Rational[] _gpsDatestamp = null;
+ private String _originalTimestamp = null;
+ private byte[] _thumbnail = null;
private ArrayList _errors = null;
}
/**
- * Set the timestamp
+ * Set the Gps timestamp
* @param inValues array of Rationals holding timestamp
*/
- public void setTimestamp(Rational[] inValues)
+ public void setGpsTimestamp(Rational[] inValues)
{
- _timestamp = inValues;
+ _gpsTimestamp = inValues;
}
/**
- * Set the datestamp
+ * Set the Gps datestamp
* @param inValues array of Rationals holding datestamp
*/
- public void setDatestamp(Rational[] inValues)
+ public void setGpsDatestamp(Rational[] inValues)
{
- _datestamp = inValues;
+ _gpsDatestamp = inValues;
+ }
+
+ /**
+ * Set the original timestamp
+ * @param inStamp original timestamp of photo
+ */
+ public void setOriginalTimestamp(String inStamp)
+ {
+ _originalTimestamp = inStamp;
}
/** @return latitude ref as char */
public byte getAltitudeRef() { return _altitudeRef; }
/** @return altitude as Rational */
public Rational getAltitude() { return _altitude; }
- /** @return timestamp as array of 3 Rationals */
- public Rational[] getTimestamp() { return _timestamp; }
- /** @return timestamp as array of 3 Rationals */
- public Rational[] getDatestamp() { return _datestamp; }
+ /** @return Gps timestamp as array of 3 Rationals */
+ public Rational[] getGpsTimestamp() { return _gpsTimestamp; }
+ /** @return Gps datestamp as array of 3 Rationals */
+ public Rational[] getGpsDatestamp() { return _gpsDatestamp; }
+ /** @return original timestamp as string */
+ public String getOriginalTimestamp() { return _originalTimestamp; }
+
+ /**
+ * Set the thumbnail
+ * @param inBytes byte array containing thumbnail
+ */
+ public void setThumbnailImage(byte[] inBytes) {
+ _thumbnail = inBytes;
+ }
+ /** @return thumbnail as byte array */
+ public byte[] getThumbnailImage() {
+ return _thumbnail;
+ }
/**
* @return true if data looks valid, ie has at least lat and long
*/\r
public void addSegment(byte inSegmentMarker, byte[] inSegmentBytes)\r
{\r
- // System.out.println("Adding segment: " + inSegmentMarker);\r
List segmentList = getOrCreateSegmentList(inSegmentMarker);\r
segmentList.add(inSegmentBytes);\r
}\r
}\r
\r
/**\r
- * Checks if this rational number is an Integer, either positive or negative.\r
+ * Checks if this rational number is an Integer, either positive or negative\r
+ * @return true if an integer\r
*/\r
public boolean isInteger()\r
{\r
/**
* Constructor giving list size
+ * @param inSize number of fields
*/
public EditFieldsTableModel(int inSize)
{
{
return _fieldValues[inRowIndex];
}
- return new Boolean(_valueChanged[inRowIndex]);
+ return Boolean.valueOf(_valueChanged[inRowIndex]);
}
/**
* Constructor
+ * @param inParent parent frame
*/
public AboutScreen(JFrame inParent)
{
new JLabel("Eclipse"),
1, 2);
addToGridBagPanel(creditsPanel, gridBag, constraints,
- new JLabel(I18nManager.getText("dialog.about.credits.translations") + " : "),
+ new JLabel(I18nManager.getText("dialog.about.credits.translators") + " : "),
0, 3);
addToGridBagPanel(creditsPanel, gridBag, constraints,
- new JLabel("Open Office, Gpsdrive, Babelfish, Leo"),
+ new JLabel("Ramon, Miguel, Inés, Piotr"),
1, 3);
addToGridBagPanel(creditsPanel, gridBag, constraints,
- new JLabel(I18nManager.getText("dialog.about.credits.devtools") + " : "),
+ new JLabel(I18nManager.getText("dialog.about.credits.translations") + " : "),
0, 4);
addToGridBagPanel(creditsPanel, gridBag, constraints,
- new JLabel("Mandriva Linux, Sun Java, Eclipse, Svn, Gimp"),
+ new JLabel("Open Office, Gpsdrive, Babelfish, Leo, Launchpad"),
1, 4);
addToGridBagPanel(creditsPanel, gridBag, constraints,
- new JLabel(I18nManager.getText("dialog.about.credits.othertools") + " : "),
+ new JLabel(I18nManager.getText("dialog.about.credits.devtools") + " : "),
0, 5);
addToGridBagPanel(creditsPanel, gridBag, constraints,
- new JLabel("Garble, Kate, Povray, Inkscape, Google Earth"),
+ new JLabel("Mandriva Linux, Sun Java, Eclipse, Svn, Gimp"),
1, 5);
addToGridBagPanel(creditsPanel, gridBag, constraints,
- new JLabel(I18nManager.getText("dialog.about.credits.thanks") + " : "),
+ new JLabel(I18nManager.getText("dialog.about.credits.othertools") + " : "),
0, 6);
addToGridBagPanel(creditsPanel, gridBag, constraints,
- new JLabel("Friends and loved ones, for encouragement and support"),
+ new JLabel("Garble, Kate, Povray, Exiftool, Inkscape, Google Earth"),
1, 6);
+ addToGridBagPanel(creditsPanel, gridBag, constraints,
+ new JLabel(I18nManager.getText("dialog.about.credits.thanks") + " : "),
+ 0, 7);
+ addToGridBagPanel(creditsPanel, gridBag, constraints,
+ new JLabel("Friends and loved ones, for encouragement and support"),
+ 1, 7);
tabPane.add(I18nManager.getText("dialog.about.credits"), creditsPanel);
// OK button at the bottom
import tim.prune.data.Distance;
import tim.prune.data.IntegerRange;
import tim.prune.data.Photo;
+import tim.prune.data.PhotoStatus;
import tim.prune.data.Selection;
import tim.prune.data.TrackInfo;
// Photo details
private JLabel _photoLabel = null;
private PhotoThumbnail _photoThumbnail = null;
+ private JLabel _photoConnectedLabel = null;
// Units
- private JComboBox _unitsDropdown = null;
+ private JComboBox _coordFormatDropdown = null;
+ private JComboBox _distUnitsDropdown = null;
// Formatter
private NumberFormat _distanceFormatter = NumberFormat.getInstance();
photoDetailsPanel.add(photoDetailsLabel);
_photoLabel = new JLabel(I18nManager.getText("details.nophoto"));
photoDetailsPanel.add(_photoLabel);
+ _photoConnectedLabel = new JLabel("");
+ photoDetailsPanel.add(_photoConnectedLabel);
_photoThumbnail = new PhotoThumbnail();
_photoThumbnail.setVisible(false);
_photoThumbnail.setPreferredSize(new Dimension(100, 100));
// add the main panel at the top
add(mainPanel, BorderLayout.NORTH);
- // Add units selection
+ // Add format, units selection
JPanel lowerPanel = new JPanel();
lowerPanel.setLayout(new BoxLayout(lowerPanel, BoxLayout.Y_AXIS));
+ JLabel coordFormatLabel = new JLabel(I18nManager.getText("details.coordformat") + ": ");
+ coordFormatLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
+ lowerPanel.add(coordFormatLabel);
+ String[] coordFormats = {I18nManager.getText("units.original"), I18nManager.getText("units.degminsec"),
+ I18nManager.getText("units.degmin"), I18nManager.getText("units.deg")};
+ _coordFormatDropdown = new JComboBox(coordFormats);
+ _coordFormatDropdown.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ dataUpdated(DataSubscriber.UNITS_CHANGED);
+ }
+ });
+ lowerPanel.add(_coordFormatDropdown);
+ _coordFormatDropdown.setAlignmentX(Component.LEFT_ALIGNMENT);
JLabel unitsLabel = new JLabel(I18nManager.getText("details.distanceunits") + ": ");
unitsLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
lowerPanel.add(unitsLabel);
String[] distUnits = {I18nManager.getText("units.kilometres"), I18nManager.getText("units.miles")};
- _unitsDropdown = new JComboBox(distUnits);
- _unitsDropdown.addActionListener(new ActionListener() {
+ _distUnitsDropdown = new JComboBox(distUnits);
+ _distUnitsDropdown.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e)
{
dataUpdated(DataSubscriber.UNITS_CHANGED);
}
});
- lowerPanel.add(_unitsDropdown);
- _unitsDropdown.setAlignmentX(Component.LEFT_ALIGNMENT);
+ lowerPanel.add(_distUnitsDropdown);
+ _distUnitsDropdown.setAlignmentX(Component.LEFT_ALIGNMENT);
add(lowerPanel, BorderLayout.SOUTH);
}
/**
* Notification that Track has been updated
+ * @param inUpdateType byte to specify what has been updated
*/
public void dataUpdated(byte inUpdateType)
{
_indexLabel.setText(LABEL_POINT_SELECTED1
+ (currentPointIndex+1) + " " + I18nManager.getText("details.index.of")
+ " " + _track.getNumPoints());
- _latLabel.setText(LABEL_POINT_LATITUDE + currentPoint.getLatitude().output(Coordinate.FORMAT_NONE));
- _longLabel.setText(LABEL_POINT_LONGITUDE + currentPoint.getLongitude().output(Coordinate.FORMAT_NONE));
+ _latLabel.setText(makeCoordinateLabel(LABEL_POINT_LATITUDE, currentPoint.getLatitude(), _coordFormatDropdown.getSelectedIndex()));
+ _longLabel.setText(makeCoordinateLabel(LABEL_POINT_LONGITUDE, currentPoint.getLongitude(), _coordFormatDropdown.getSelectedIndex()));
_altLabel.setText(LABEL_POINT_ALTITUDE
+ (currentPoint.hasAltitude()?
(currentPoint.getAltitude().getValue() + getAltitudeUnitsLabel(currentPoint.getAltitude().getFormat())):
_rangeLabel.setText(LABEL_RANGE_SELECTED1
+ (selection.getStart()+1) + " " + I18nManager.getText("details.range.to")
+ " " + (selection.getEnd()+1));
- if (_unitsDropdown.getSelectedIndex() == 0)
+ if (_distUnitsDropdown.getSelectedIndex() == 0)
_distanceLabel.setText(LABEL_RANGE_DISTANCE + buildDistanceString(
selection.getDistance(Distance.UNITS_KILOMETRES))
+ " " + I18nManager.getText("units.kilometres.short"));
{
// no photo, hide details
_photoLabel.setText(I18nManager.getText("details.nophoto"));
+ _photoConnectedLabel.setText("");
_photoThumbnail.setVisible(false);
}
else
{
if (currentPhoto == null) {currentPhoto = currentPoint.getPhoto();}
_photoLabel.setText(I18nManager.getText("details.photofile") + ": " + currentPhoto.getFile().getName());
+ _photoConnectedLabel.setText(I18nManager.getText("details.photo.connected") + ": "
+ + (currentPhoto.getCurrentStatus() == PhotoStatus.NOT_CONNECTED ?
+ I18nManager.getText("dialog.about.no"):I18nManager.getText("dialog.about.yes")));
_photoThumbnail.setVisible(true);
_photoThumbnail.setPhoto(currentPhoto);
}
}
+ /**
+ * Construct an appropriate coordinate label using the selected format
+ * @param inPrefix prefix of label
+ * @param inCoordinate coordinate
+ * @param inFormat index of format selection dropdown
+ * @return language-sensitive string
+ */
+ private static String makeCoordinateLabel(String inPrefix, Coordinate inCoordinate, int inFormat)
+ {
+ String coord = null;
+ switch (inFormat) {
+ case 1: // degminsec
+ coord = inCoordinate.output(Coordinate.FORMAT_DEG_MIN_SEC); break;
+ case 2: // degmin
+ coord = inCoordinate.output(Coordinate.FORMAT_DEG_MIN); break;
+ case 3: // degrees
+ coord = inCoordinate.output(Coordinate.FORMAT_DEG); break;
+ default: // just as it was
+ coord = inCoordinate.output(Coordinate.FORMAT_NONE);
+ }
+ return inPrefix + coord;
+ }
+
+
/**
* Build a String to describe a time duration
* @param inNumSecs number of seconds
private BufferedImage _image = null;
private JPopupMenu _popup = null;
private JCheckBoxMenuItem _autoPanMenuItem = null;
+ private JCheckBoxMenuItem _connectPointsMenuItem = null;
private int _numPoints = -1;
private double _scale;
private double _offsetX, _offsetY, _zoomScale;
/**
* Override paint method to draw map
+ * @param inG graphics object
*/
- public void paint(Graphics g)
+ public void paint(Graphics inG)
{
if (_track == null)
{
- super.paint(g);
+ super.paint(inG);
return;
}
if (_image == null) {return;}
// draw buffered image onto g
- g.drawImage(_image, 0, 0, width, height, COLOR_BG, null);
+ inG.drawImage(_image, 0, 0, width, height, COLOR_BG, null);
// draw selected range, if any
if (_trackInfo.getSelection().hasRangeSelected() && !_zoomDragging)
{
int rangeStart = _trackInfo.getSelection().getStart();
int rangeEnd = _trackInfo.getSelection().getEnd();
- g.setColor(COLOR_CURR_RANGE);
+ inG.setColor(COLOR_CURR_RANGE);
for (int i=rangeStart; i<=rangeEnd; i++)
{
x = width/2 + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
&& y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
{
- g.drawOval(x - 2, y - 2, 4, 4);
+ inG.drawRect(x - 2, y - 2, 4, 4);
}
}
}
// Highlight selected point
if (selectedPoint >= 0 && !_zoomDragging)
{
- g.setColor(COLOR_CROSSHAIRS);
+ inG.setColor(COLOR_CROSSHAIRS);
x = width/2 + (int) ((_track.getX(selectedPoint) - _offsetX) / _scale * _zoomScale);
y = height/2 - (int) ((_track.getY(selectedPoint) - _offsetY) / _scale * _zoomScale);
if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
&& y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
{
// Draw cross-hairs for current point
- g.drawLine(x, BORDER_WIDTH, x, height - BORDER_WIDTH);
- g.drawLine(BORDER_WIDTH, y, width - BORDER_WIDTH, y);
+ inG.drawLine(x, BORDER_WIDTH, x, height - BORDER_WIDTH);
+ inG.drawLine(BORDER_WIDTH, y, width - BORDER_WIDTH, y);
// Show selected point afterwards to make sure it's on top
- g.drawOval(x - 2, y - 2, 4, 4);
- g.drawOval(x - 3, y - 3, 6, 6);
+ inG.drawOval(x - 2, y - 2, 4, 4);
+ inG.drawOval(x - 3, y - 3, 6, 6);
}
}
// Draw rectangle for dragging zoom area
if (_zoomDragging)
{
- g.setColor(COLOR_CROSSHAIRS);
- g.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragFromX, _zoomDragToY);
- g.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragToX, _zoomDragFromY);
- g.drawLine(_zoomDragToX, _zoomDragFromY, _zoomDragToX, _zoomDragToY);
- g.drawLine(_zoomDragFromX, _zoomDragToY, _zoomDragToX, _zoomDragToY);
+ inG.setColor(COLOR_CROSSHAIRS);
+ inG.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragFromX, _zoomDragToY);
+ inG.drawLine(_zoomDragFromX, _zoomDragFromY, _zoomDragToX, _zoomDragFromY);
+ inG.drawLine(_zoomDragToX, _zoomDragFromY, _zoomDragToX, _zoomDragToY);
+ inG.drawLine(_zoomDragFromX, _zoomDragToY, _zoomDragToX, _zoomDragToY);
}
// Attempt to grab keyboard focus if possible
- //this.requestFocus();
+ //requestFocus(); (causes problems here)
}
/**
- * Draw the map onto an offscreen image
+ * Plot the points onto an offscreen image
+ * which doesn't have to be redrawn when the selection changes
*/
private void createBackgroundImage()
{
int width = getWidth();
int height = getHeight();
int x, y;
- // Make a new image and initialise it
- _image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+ int lastX = 0, lastY = 0;
+ // Initialise image
+ if (_image == null || _image.getWidth() != width || _image.getHeight() != height) {
+ _image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+ }
Graphics bufferedG = _image.getGraphics();
super.paint(bufferedG);
bufferedG.setColor(COLOR_POINT);
int halfWidth = width/2;
int halfHeight = height/2;
+ boolean currPointTrackpoint = false, lastPointTrackpoint = false;
for (int i=0; i<numPoints; i++)
{
x = halfWidth + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale);
if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH)
&& y < (height - BORDER_WIDTH) && y > BORDER_WIDTH)
{
- bufferedG.drawOval(x - 2, y - 2, 4, 4);
+ // draw block for point (a bit faster than circles)
+ bufferedG.drawRect(x - 2, y - 2, 3, 3);
+
+ // See whether to connect the point with previous one or not
+ currPointTrackpoint = !_track.getPoint(i).isWaypoint() && _track.getPoint(i).getPhoto() == null;
+ if (_connectPointsMenuItem.isSelected() && currPointTrackpoint && lastPointTrackpoint)
+ {
+ bufferedG.drawLine(lastX, lastY, x, y);
+ }
+ lastPointTrackpoint = currPointTrackpoint;
+ }
+ else {
+ lastPointTrackpoint = false;
}
+ lastX = x; lastY = y;
}
// Loop again and show waypoints with names
int nameWidth = fm.stringWidth(waypointName);
if (nameWidth < (width - 2 * BORDER_WIDTH))
{
- double nameAngle = 0.3;
- double nameRadius = 1.0;
boolean drawnName = false;
- while (!drawnName)
+ // Make arrays for coordinates right left up down
+ int[] nameXs = {x + 2, x - nameWidth - 2, x - nameWidth/2, x - nameWidth/2};
+ int[] nameYs = {y + (nameHeight/2), y + (nameHeight/2), y - 2, y + nameHeight + 2};
+ for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2)
{
- int nameX = x + (int) (nameRadius * Math.cos(nameAngle)) - (nameWidth/2);
- int nameY = y + (int) (nameRadius * Math.sin(nameAngle)) + (nameHeight/2);
- if (nameX > BORDER_WIDTH && (nameX + nameWidth) < (width - BORDER_WIDTH)
- && nameY < (height - BORDER_WIDTH) && (nameY - nameHeight) > BORDER_WIDTH)
+ // Shift arrays for coordinates right left up down
+ nameXs[0] += 2; nameXs[1] -= 2;
+ nameYs[2] -= 2; nameYs[3] += 2;
+ // Check each direction in turn right left up down
+ for (int a=0; a<4; a++)
{
- // name can fit in grid - does it overlap data points?
- if (!overlapsPoints(nameX, nameY, nameWidth, nameHeight) || nameRadius > 50.0)
+ if (nameXs[a] > BORDER_WIDTH && (nameXs[a] + nameWidth) < (width - BORDER_WIDTH)
+ && nameYs[a] < (height - BORDER_WIDTH) && (nameYs[a] - nameHeight) > BORDER_WIDTH
+ && !overlapsPoints(nameXs[a], nameYs[a], nameWidth, nameHeight))
{
- bufferedG.drawString(waypointName, nameX, nameY);
+ // Found a rectangle to fit - draw name here and quit
+ bufferedG.drawString(waypointName, nameXs[a], nameYs[a]);
drawnName = true;
- numWaypointNamesShown++;
+ break;
}
}
- nameAngle += 0.08;
- nameRadius += 0.2;
- // wasn't room within the radius, so don't print name
- if (nameRadius > 50.0)
- {
- drawnName = true;
- }
}
}
}
}
}
+ bufferedG.dispose();
}
}});
zoomFull.setEnabled(true);
_popup.add(zoomFull);
+ _connectPointsMenuItem = new JCheckBoxMenuItem(I18nManager.getText("menu.map.connect"));
+ _connectPointsMenuItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ // redraw map
+ dataUpdated(DataSubscriber.ALL);
+ }
+ });
+ _connectPointsMenuItem.setSelected(false);
+ _popup.add(_connectPointsMenuItem);
_autoPanMenuItem = new JCheckBoxMenuItem(I18nManager.getText("menu.map.autopan"));
_autoPanMenuItem.setSelected(true);
_popup.add(_autoPanMenuItem);
/**
* React to click on map display
+ * @param inE mouse event
*/
- public void mouseClicked(MouseEvent e)
+ public void mouseClicked(MouseEvent inE)
{
this.requestFocus();
if (_track != null)
{
- int xClick = e.getX();
- int yClick = e.getY();
+ int xClick = inE.getX();
+ int yClick = inE.getY();
// Check click is within main area (not in border)
if (xClick > BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH)
&& yClick < (getHeight() - BORDER_WIDTH))
{
// Check left click or right click
- if (e.isMetaDown())
+ if (inE.isMetaDown())
{
// Only show popup if track has data
if (_track != null && _track.getNumPoints() > 0)
- _popup.show(this, e.getX(), e.getY());
+ _popup.show(this, xClick, yClick);
}
else
{
// Menu items which need enabling/disabling
private JMenuItem _saveItem = null;
private JMenuItem _exportKmlItem = null;
+ private JMenuItem _exportGpxItem = null;
private JMenuItem _exportPovItem = null;
private JMenuItem _undoItem = null;
private JMenuItem _clearUndoItem = null;
private JMenuItem _saveExifItem = null;
private JMenuItem _connectPhotoItem = null;
private JMenuItem _deletePhotoItem = null;
- // TODO: Does Photo menu require disconnect option?
+ private JMenuItem _disconnectPhotoItem = null;
+ private JMenuItem _correlatePhotosItem = null;
// ActionListeners for reuse by menu and toolbar
private ActionListener _openFileAction = null;
* Constructor
* @param inParent parent object for dialogs
* @param inApp application to call on menu actions
+ * @param inTrackInfo track info object
*/
public MenuManager(JFrame inParent, App inApp, TrackInfo inTrackInfo)
{
_saveItem.addActionListener(_saveAction);
_saveItem.setEnabled(false);
fileMenu.add(_saveItem);
- // Export
+ // Export - Kml
_exportKmlItem = new JMenuItem(I18nManager.getText("menu.file.exportkml"));
_exportKmlItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e)
});
_exportKmlItem.setEnabled(false);
fileMenu.add(_exportKmlItem);
+ // Gpx
+ _exportGpxItem = new JMenuItem(I18nManager.getText("menu.file.exportgpx"));
+ _exportGpxItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ _app.exportGpx();
+ }
+ });
+ _exportGpxItem.setEnabled(false);
+ fileMenu.add(_exportGpxItem);
+ // Pov
_exportPovItem = new JMenuItem(I18nManager.getText("menu.file.exportpov"));
_exportPovItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e)
};
_connectPhotoItem.addActionListener(_connectPhotoAction);
_connectPhotoItem.setEnabled(false);
+ photoMenu.addSeparator();
photoMenu.add(_connectPhotoItem);
+ // disconnect photo
+ _disconnectPhotoItem = new JMenuItem(I18nManager.getText("menu.photo.disconnect"));
+ _disconnectPhotoItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ _app.disconnectPhotoFromPoint();
+ }
+ });
+ _disconnectPhotoItem.setEnabled(false);
+ photoMenu.add(_disconnectPhotoItem);
_deletePhotoItem = new JMenuItem(I18nManager.getText("menu.photo.delete"));
_deletePhotoItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e)
});
_deletePhotoItem.setEnabled(false);
photoMenu.add(_deletePhotoItem);
+ photoMenu.addSeparator();
+ // correlate all photos
+ _correlatePhotosItem = new JMenuItem(I18nManager.getText("menu.photo.correlate"));
+ _correlatePhotosItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ _app.beginCorrelatePhotos();
+ }
+ });
+ _correlatePhotosItem.setEnabled(false);
+ photoMenu.add(_correlatePhotosItem);
menubar.add(photoMenu);
// Add 3d menu (whether java3d available or not)
threeDMenu.add(_show3dItem);
menubar.add(threeDMenu);
- // Help menu for About
+ // Help menu
JMenu helpMenu = new JMenu(I18nManager.getText("menu.help"));
+ JMenuItem helpItem = new JMenuItem(I18nManager.getText("menu.help"));
+ helpItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ _app.showHelp();
+ }
+ });
+ helpMenu.add(helpItem);
JMenuItem aboutItem = new JMenuItem(I18nManager.getText("menu.help.about"));
aboutItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e)
_saveItem.setEnabled(hasData);
_saveButton.setEnabled(hasData);
_exportKmlItem.setEnabled(hasData);
+ _exportGpxItem.setEnabled(hasData);
_exportPovItem.setEnabled(hasData);
_deleteDuplicatesItem.setEnabled(hasData);
_compressItem.setEnabled(hasData);
_selectEndItem.setEnabled(hasPoint);
_selectEndButton.setEnabled(hasPoint);
// are there any photos?
- _saveExifItem.setEnabled(_photos != null && _photos.getNumPhotos() > 0);
+ boolean anyPhotos = _photos != null && _photos.getNumPhotos() > 0;
+ _saveExifItem.setEnabled(anyPhotos);
// is there a current photo?
- boolean hasPhoto = _photos != null && _photos.getNumPhotos() > 0
- && _selection.getCurrentPhotoIndex() >= 0;
+ boolean hasPhoto = anyPhotos && _selection.getCurrentPhotoIndex() >= 0;
// connect is only available when current photo is not connected to current point
boolean connectAvailable = hasPhoto && hasPoint
&& _track.getPoint(_selection.getCurrentPointIndex()).getPhoto() == null;
_connectPhotoItem.setEnabled(connectAvailable);
_connectPhotoButton.setEnabled(connectAvailable);
+ _disconnectPhotoItem.setEnabled(hasPhoto && _photos.getPhoto(_selection.getCurrentPhotoIndex()) != null
+ && _photos.getPhoto(_selection.getCurrentPhotoIndex()).getDataPoint() != null);
+ _correlatePhotosItem.setEnabled(anyPhotos && hasData);
_deletePhotoItem.setEnabled(hasPhoto);
// is there a current range?
boolean hasRange = (hasData && _selection.hasRangeSelected());
import java.awt.Graphics;
import java.awt.Image;
import java.awt.image.BufferedImage;
-
import javax.swing.ImageIcon;
import javax.swing.JPanel;
*/
public void run()
{
- int picWidth = _photo.getWidth();
- int picHeight = _photo.getHeight();
- if (picWidth > -1 && picHeight > -1)
+ // Use exif thumbnail?
+ if (_photo.getExifThumbnail() != null) {
+ Image image = new ImageIcon(_photo.getExifThumbnail()).getImage();
+ _thumbnail = ImageUtils.createScaledImage(image, image.getWidth(null), image.getHeight(null));
+ image = null;
+ }
+ else
{
- int displayWidth = Math.min(getWidth(), getParent().getWidth());
- // System.out.println("width = " + getWidth() + ", " + getParent().getWidth() + " = " + displayWidth);
- int displayHeight = Math.min(getHeight(), getParent().getHeight());
- // System.out.println("height = " + getHeight() + ", " + getParent().getHeight() + " = " + displayHeight);
-
- // calculate maximum thumbnail size
- Dimension thumbSize = ImageUtils.getThumbnailSize(picWidth, picHeight, displayWidth, displayHeight);
- // Work out if need to remake image
- boolean needToRemake = (_thumbnail == null)
- || _thumbnail.getWidth() != thumbSize.width || _thumbnail.getHeight() != thumbSize.height;
- if (thumbSize.width > 0 && thumbSize.height > 0 && needToRemake)
+ // no exif thumbnail available, going to have to read whole thing
+ int picWidth = _photo.getWidth();
+ int picHeight = _photo.getHeight();
+ if (picWidth > -1 && picHeight > -1)
{
- // Make icon to load image into
- Image image = new ImageIcon(_photo.getFile().getAbsolutePath()).getImage();
- // save scaled, smoothed thumbnail for reuse
- _thumbnail = ImageUtils.createScaledImage(image, thumbSize.width, thumbSize.height);
- image = null;
- // TODO: Calculate and set size of thumbnail here
- // setPreferredSize(new Dimension(200, 200));
+ int displayWidth = Math.min(getWidth(), getParent().getWidth());
+ // System.out.println("width = " + getWidth() + ", " + getParent().getWidth() + " = " + displayWidth);
+ int displayHeight = Math.min(getHeight(), getParent().getHeight());
+ // System.out.println("height = " + getHeight() + ", " + getParent().getHeight() + " = " + displayHeight);
+
+ // calculate maximum thumbnail size
+ Dimension thumbSize = ImageUtils.getThumbnailSize(picWidth, picHeight, displayWidth, displayHeight);
+ // Work out if need to remake image
+ boolean needToRemake = (_thumbnail == null)
+ || _thumbnail.getWidth() != thumbSize.width || _thumbnail.getHeight() != thumbSize.height;
+ if (thumbSize.width > 0 && thumbSize.height > 0 && needToRemake)
+ {
+ // Make icon to load image into
+ Image image = new ImageIcon(_photo.getFile().getAbsolutePath()).getImage();
+ // save scaled, smoothed thumbnail for reuse
+ _thumbnail = ImageUtils.createScaledImage(image, thumbSize.width, thumbSize.height);
+ image = null;
+ // TODO: Calculate and set size of thumbnail here
+ // setPreferredSize(new Dimension(200, 200));
+ }
}
}
_loadingImage = false;
/**
* Method to inform map that data has changed
+ * @param inTrack track object
*/
public void dataUpdated(Track inTrack)
{
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
+import java.awt.GridLayout;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
// Add panel for waypoints / photos
JPanel listsPanel = new JPanel();
- listsPanel.setLayout(new BoxLayout(listsPanel, BoxLayout.Y_AXIS));
+ listsPanel.setLayout(new GridLayout(0, 1));
listsPanel.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3))
);
{
if (!e.getValueIsAdjusting()) selectWaypoint(_waypointList.getSelectedIndex());
}});
- listsPanel.add(new JLabel(I18nManager.getText("details.waypointsphotos.waypoints")));
- listsPanel.add(new JScrollPane(_waypointList));
+ JPanel waypointListPanel = new JPanel();
+ waypointListPanel.setLayout(new BorderLayout());
+ waypointListPanel.add(new JLabel(I18nManager.getText("details.waypointsphotos.waypoints")), BorderLayout.NORTH);
+ waypointListPanel.add(new JScrollPane(_waypointList), BorderLayout.CENTER);
+ listsPanel.add(waypointListPanel);
+ // photo list
_photoListModel = new PhotoListModel(_trackInfo.getPhotoList());
_photoList = new JList(_photoListModel);
_photoList.setVisibleRowCount(NUM_LIST_ENTRIES);
{
if (!e.getValueIsAdjusting()) selectPhoto(_photoList.getSelectedIndex());
}});
- listsPanel.add(new JLabel(I18nManager.getText("details.waypointsphotos.photos")));
- listsPanel.add(new JScrollPane(_photoList));
+ JPanel photoListPanel = new JPanel();
+ photoListPanel.setLayout(new BorderLayout());
+ photoListPanel.add(new JLabel(I18nManager.getText("details.waypointsphotos.photos")), BorderLayout.NORTH);
+ photoListPanel.add(new JScrollPane(_photoList), BorderLayout.CENTER);
+ listsPanel.add(photoListPanel);
listsPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
// add the controls to the main panel
mainPanel.add(Box.createVerticalStrut(5));
mainPanel.add(_scroller);
mainPanel.add(Box.createVerticalStrut(5));
- mainPanel.add(listsPanel);
// add the main panel at the top
add(mainPanel, BorderLayout.NORTH);
+ // and lists in the centre
+ add(listsPanel, BorderLayout.CENTER);
// set preferred width to be small
setPreferredSize(new Dimension(100, 100));
}
/**
* Constructor
+ * @param inApp App object
+ * @param inFrame parent frame
*/
public UndoManager(App inApp, JFrame inFrame)
{
// Buttons
JPanel buttonPanel = new JPanel();
buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
- JButton okButton = new JButton("OK");
+ JButton okButton = new JButton(I18nManager.getText("button.ok"));
okButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
}
});
buttonPanel.add(okButton);
- JButton cancelButton = new JButton("Cancel");
+ JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
cancelButton.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
# Menu entries
menu.file=File
menu.file.open=Open
-menu.file.addphotos=Add Photos
+menu.file.addphotos=Add photos
menu.file.save=Save
menu.file.exportkml=Export KML
+menu.file.exportgpx=Export GPX
menu.file.exportpov=Export POV
menu.file.exit=Exit
menu.edit=Edit
menu.photo=Photo
menu.photo.saveexif=Save to Exif
menu.photo.connect=Connect to point
+menu.photo.disconnect=Disconnect from point
+menu.photo.correlate=Correlate all photos
menu.photo.delete=Remove photo
menu.3d=Three-D
menu.3d.show3d=Show in Three-D
menu.map.zoomin=Zoom in
menu.map.zoomout=Zoom out
menu.map.zoomfull=Zoom to full scale
+menu.map.connect=Connect track points
menu.map.autopan=Autopan
# Dialogs
dialog.save.table.save=Save
dialog.save.headerrow=Output header row
dialog.save.coordinateunits=Coordinate units
-dialog.save.units.original=Original
dialog.save.altitudeunits=Altitude units
+dialog.save.timestampformat=Timestamp format
dialog.save.oktitle=File saved
dialog.save.ok1=Successfully saved
dialog.save.ok2=points to file
dialog.save.overwrite.text=This file already exists. Are you sure you want to overwrite the file?
dialog.exportkml.title=Export KML
dialog.exportkml.text=Title for the data
+dialog.exportkml.altitude=Include altitudes (for aviation)
dialog.exportkml.kmz=Compress to make kmz file
dialog.exportkml.exportimages=Export image thumbnails to kmz
dialog.exportkml.filetype=KML, KMZ files
+dialog.exportgpx.title=Export GPX
+dialog.exportgpx.name=Name
+dialog.exportgpx.desc=Description
+dialog.exportgpx.filetype=GPX files
dialog.exportpov.title=Export POV
dialog.exportpov.text=Please enter the parameters for the POV export
dialog.exportpov.font=Font
dialog.saveexif.overwrite=Overwrite files
dialog.saveexif.ok1=Saved
dialog.saveexif.ok2=photo files
+dialog.correlate.title=Correlate photos
+dialog.correlate.notimestamps=There are no timestamps in the data points, so there is nothing to correlate with the photos.
+dialog.correlate.nouncorrelatedphotos=There are no uncorrelated photos.\nAre you sure you want to continue?
+dialog.correlate.photoselect.intro=Select one of these correlated photos to use as the time offset
+dialog.correlate.photoselect.photoname=Photo name
+dialog.correlate.photoselect.timediff=Time difference
+dialog.correlate.photoselect.photolater=Photo later
+dialog.correlate.options.tip=Tip: By manually correlating at least one photo, the time offset can be calculated for you.
+dialog.correlate.options.intro=Select the options for automatic correlation
+dialog.correlate.options.offsetpanel=Time offset
+dialog.correlate.options.offset=Offset
+dialog.correlate.options.offset.hours=hours,
+dialog.correlate.options.offset.minutes=minutes and
+dialog.correlate.options.offset.seconds=seconds
+dialog.correlate.options.photolater=Photo later than point
+dialog.correlate.options.pointlater=Point later than photo
+dialog.correlate.options.limitspanel=Correlation limits
+dialog.correlate.options.notimelimit=No time limit
+dialog.correlate.options.timelimit=Time limit
+dialog.correlate.options.nodistancelimit=No distance limit
+dialog.correlate.options.distancelimit=Distance limit
+dialog.correlate.options.correlate=Correlate
+dialog.correlate.alloutsiderange=All photos are outside the time range of the track, so none can be correlated.\nTry changing the offset or manually correlating at least one photo.
+dialog.correlate.confirmsingle.text=photo was correlated
+dialog.correlate.confirmmultiple.text=photos were correlated
+dialog.help.help=Please see\n http://activityworkshop.net/software/prune/\nfor more information and user guides.
dialog.about.title=About Prune
dialog.about.version=Version
dialog.about.build=Build
dialog.about.credits.code=Prune code written by
dialog.about.credits.exifcode=Exif code by
dialog.about.credits.icons=Some icons taken from
+dialog.about.credits.translators=Translators
dialog.about.credits.translations=Translations helped by
dialog.about.credits.devtools=Development tools
dialog.about.credits.othertools=Other tools
button.overwrite=Overwrite
button.moveup=Move up
button.movedown=Move down
-button.deletepoint=Delete point
-button.deleterange=Delete range
button.showlines=Show lines
button.edit=Edit
button.exit=Exit
button.notoall=No to all
button.selectall=Select all
button.selectnone=Select none
+button.preview=Preview
+button.guessfields=Guess fields
# Display components
display.nodata=No data loaded
details.altitude.to=to
details.range.climb=Climb
details.range.descent=Descent
+details.coordformat=Coordinate format
details.distanceunits=Distance units
display.range.time.secs=s
display.range.time.mins=m
details.photodetails=Photo details
details.nophoto=No photo selected
details.photo.loading=Loading
+details.photo.connected=Connected
# Field names
fieldname.latitude=Latitude
fieldname.duration=Duration
# Measurement units
+units.original=Original
+units.default=Default
units.metres=Metres
units.metres.short=m
units.feet=Feet
units.degminsec=Deg-min-sec
units.degmin=Deg-min
units.deg=Degrees
+units.iso8601=ISO 8601
# Cardinals for 3d plots
cardinal.n=N
undo.reverse=reverse range
undo.rearrangewaypoints=rearrange waypoints
undo.connectphoto=connect photo
+undo.disconnectphoto=disconnect photo
+undo.correlate=correlate photos
# Error messages
error.save.dialogtitle=Error saving data
menu.file.addphotos=Fotos laden
menu.file.save=Speichern
menu.file.exportkml=KML exportieren
+menu.file.exportgpx=GPX exportieren
menu.file.exportpov=POV exportieren
menu.file.exit=Beenden
menu.edit=Bearbeiten
menu.photo=Foto
menu.photo.saveexif=Exif Daten speichern
menu.photo.connect=Mit Punkt verbinden
+menu.photo.disconnect=Vom Punkt trennen
+menu.photo.correlate=Alle Fotos korrelieren
menu.photo.delete=Foto entfernen
menu.3d=Drei-D
menu.3d.show3d=In drei-D zeigen
menu.map.zoomin=Einzoomen
menu.map.zoomout=Auszoomen
menu.map.zoomfull=Zoomen zum ganzes Bild
+menu.map.connect=Trackpunkte mit Linie
menu.map.autopan=Autopan
# Dialogs
dialog.save.table.save=Speichern
dialog.save.headerrow=Titel Zeile speichern
dialog.save.coordinateunits=Koordinaten Maßeinheiten
-dialog.save.units.original=Original
dialog.save.altitudeunits=Höhe Maßeinheiten
+dialog.save.timestampformat=Zeitstempelformat
dialog.save.oktitle=Datei gespeichert
dialog.save.ok1=Es wurden
dialog.save.ok2=Punkte gespeichert nach
dialog.save.overwrite.text=Diese Datei existiert schon. Sind Sie sicher, Sie wollen die Datei überschreiben?
dialog.exportkml.title=KML exportieren
dialog.exportkml.text=Titel für die Daten
+dialog.exportkml.altitude=Auch Höheninformation (für Luftfahrt)
dialog.exportkml.kmz=Daten ins kmz Datei komprimieren
dialog.exportkml.exportimages=Bilder ins kmz exportieren
dialog.exportkml.filetype=KML, KMZ Dateien
+dialog.exportgpx.title=GPX exportieren
+dialog.exportgpx.name=Name
+dialog.exportgpx.desc=Beschreibung
+dialog.exportgpx.filetype=GPX Dateien
dialog.exportpov.title=POV exportieren
dialog.exportpov.text=Geben Sie die Parameter ein für das POV Export
dialog.exportpov.font=Font
dialog.saveexif.title=Exif speichern
dialog.saveexif.intro=Selektieren Sie die Fotos zu speichern
dialog.saveexif.nothingtosave=Koordinaten sind nicht modifiziert, nichts zu speichern
-dialog.saveexif.noexiftool=Kein exiftool Program gefunden. Trotzdem fortfahren?
+dialog.saveexif.noexiftool=Kein exiftool Programm gefunden. Trotzdem fortfahren?
dialog.saveexif.table.photoname=Foto Name
dialog.saveexif.table.status=Status
dialog.saveexif.table.save=Speichern
dialog.saveexif.overwrite=Dateien überschreiben
dialog.saveexif.ok1=Es wurden
dialog.saveexif.ok2=Foto Dateien geschrieben
+dialog.correlate.title=Fotos korrelieren
+dialog.correlate.notimestamps=Die Punkte haben keine Zeitinformation, deswegen ist es nicht möglich die Fotos zu korrelieren.
+dialog.correlate.nouncorrelatedphotos=Alle Photos sind schon korreliert.\nWollen Sie trotzdem fortsetzen?
+dialog.correlate.photoselect.intro=Selektieren Sie einen von diesen Fotos um die Differenz zu berechnen
+dialog.correlate.photoselect.photoname=Foto Name
+dialog.correlate.photoselect.timediff=Zeitdifferenz
+dialog.correlate.photoselect.photolater=Foto später
+dialog.correlate.options.tip=Tipp: Mit mindestens einem korrelierten Foto, die Zeitdifferenz kann automatisch berechnet werden.
+dialog.correlate.options.intro=Wählen Sie die Optionen aus für die Korrelation
+dialog.correlate.options.offsetpanel=Zeitunterschied
+dialog.correlate.options.offset=Unterschied
+dialog.correlate.options.offset.hours=Stunden,
+dialog.correlate.options.offset.minutes=Minuten und
+dialog.correlate.options.offset.seconds=Sekunden
+dialog.correlate.options.photolater=Foto später als Punkt
+dialog.correlate.options.pointlater=Punkt später als Foto
+dialog.correlate.options.limitspanel=Korrelation Grenzen
+dialog.correlate.options.notimelimit=Keine Zeitgrenzen
+dialog.correlate.options.timelimit=Zeitgrenzen
+dialog.correlate.options.nodistancelimit=Keine Distanzgrenzen
+dialog.correlate.options.distancelimit=Distanzgrenzen
+dialog.correlate.options.correlate=Korrelieren
+dialog.correlate.alloutsiderange=Alle Fotos sind ausserhalb vom Track Zeitraum, so können nicht korreliert werden.\nVersuchen Sie mit einem anderen Offset oder verbinden Sie manuell mindestens ein Foto.
+dialog.correlate.confirmsingle.text=Foto wurde korreliert
+dialog.correlate.confirmmultiple.text=Fotos wurden korreliert
+dialog.help.help=Bitte sehen Sie\n http://activityworkshop.net/software/prune/\nfür weitere Information und Benutzeranleitungen.
dialog.about.title=Ãœber Prune
dialog.about.version=Version
dialog.about.build=Build
dialog.about.credits.code=Prune Code geschrieben von
dialog.about.credits.exifcode=Exif Code von
dialog.about.credits.icons=Einige Ikons von
+dialog.about.credits.translators=Dolmetscher
dialog.about.credits.translations=Ãœbersetzungen mit Hilfe von
dialog.about.credits.devtools=Entwicklungsprogrammen
dialog.about.credits.othertools=Andere Programmen
button.overwrite=Ãœberschreiben
button.moveup=Aufwärts moven
button.movedown=Abwärts moven
-button.deletepoint=Punkt löschen
-button.deleterange=Spanne löschen
button.showlines=Linien anzeigen
button.edit=Bearbeiten
button.exit=Beenden
button.notoall=Nein für alle
button.selectall=Alle selektieren
button.selectnone=Nichts selektieren
+button.preview=Vorschauen
+button.guessfields=Felder erraten
# Display components
display.nodata=Keine Daten geladen
details.altitude.to=bis
details.range.climb=Aufstieg
details.range.descent=Abstieg
+details.coordformat=Koordinatenformat
details.distanceunits=Distanz Maßeinheiten
display.range.time.secs=S
display.range.time.mins=M
details.photodetails=Details vom Foto
details.nophoto=Kein Foto selektiert
details.photo.loading=Laden
+details.photo.connected=Verbunden
# Field names
fieldname.latitude=Breitengrad
fieldname.duration=Zeitlänge
# Measurement units
+units.original=Original
+units.default=Default
units.metres=Meter
units.metres.short=M
units.feet=Füße
units.degminsec=Grad-Min-Sek
units.degmin=Grad-Min
units.deg=Grad
+units.iso8601=ISO 8601
# Cardinals for 3d plots
cardinal.n=N
undo.reverse=Spanne umdrehen
undo.rearrangewaypoints=Waypoints reorganisieren
undo.connectphoto=Foto verbinden
+undo.disconnectphoto=Foto trennen
+undo.correlate=Fotos korrelieren
# Error messages
error.save.dialogtitle=Fehler beim Speichern
menu.file.addphotos=Fötelis innätue
menu.file.save=Speichere
menu.file.exportkml=KML exportiere
+menu.file.exportgpx=GPX exportiere
menu.file.exportpov=POV exportiere
menu.file.exit=Beände
menu.edit=Editiere
menu.photo=Föteli
menu.photo.saveexif=Exif Date speicherä
menu.photo.connect=Mitem Punkt verbindä
+menu.photo.disconnect=Vonem Punkt trännä
+menu.photo.correlate=Alli Fötelis korrelierä
menu.photo.delete=Föteli entfernä
menu.3d=Drüü-D
menu.3d.show3d=In drüü-D zeigä
menu.map.zoomin=Einzoome
menu.map.zoomout=Uuszoome
menu.map.zoomfull=Zoome zum ganzes Bild
+menu.map.connect=Trackpünktli verbindä
menu.map.autopan=Autopan
# Dialogs
dialog.save.table.save=Speicherä
dialog.save.headerrow=Titel Ziile speicherä
dialog.save.coordinateunits=Koordinate Massiiheite
-dialog.save.units.original=Original
dialog.save.altitudeunits=Höchi Massiiheite
-dialog.save.oktitle=File gespeichert worde
+dialog.save.timestampformat=Ziitstämpelformat
+dialog.save.oktitle=File gspeicheret worde
dialog.save.ok1=Es sin
dialog.save.ok2=Punkte gspeicheret worde na
dialog.save.overwrite.title=s'File existiert scho
dialog.save.overwrite.text=s'File existiert scho. Sind Sie sicher, Sie wend s'File überschriibe?
dialog.exportkml.title=KML exportierä
dialog.exportkml.text=Titel für die Date
+dialog.exportkml.altitude=Au Höchiinformation (fürs Fliege)
dialog.exportkml.kmz=Date ins kmz File komprimierä
dialog.exportkml.exportimages=Bildli ins Kmz exportierä
dialog.exportkml.filetype=KML, KMZ Dateie
-dialog.exportpov.title=POV exportiere
+dialog.exportgpx.title=GPX exportierä
+dialog.exportgpx.name=Name
+dialog.exportgpx.desc=Beschriibig
+dialog.exportgpx.filetype=GPX Dateie
+dialog.exportpov.title=POV exportierä
dialog.exportpov.text=Gäbet Sie die Parameter ii fürs POV Export
dialog.exportpov.font=Font
dialog.exportpov.camerax=Kamera X
dialog.exportpov.cameray=Kamera Y
dialog.exportpov.cameraz=Kamera Z
dialog.exportpov.filetype=POV Dateie
-dialog.exportpov.warningtracksize=Dieser Track hät sehr viele Punkte, die Java3D villiicht nöd chann bearbeite.\nSind Sie sicher, Sie wend trotzdem fortsetze?
+dialog.exportpov.warningtracksize=Dieser Track hät mega viele Punkte, die Java3D villiicht nöd chann bearbeite.\nSind Sie sicher, Sie wend trotzdem fortsetze?
dialog.confirmreversetrack.title=Umdrehig bestätige
dialog.confirmreversetrack.text=Diese Daten enthalte Ziitstämpel Informatione, die bei dr Umkehrig usser Reihefolge erschiene würdi.\nSind Sie sicher, Sie wend diese Spanne umkehre?
dialog.interpolate.title=Punkte interpoliere
dialog.confirmundo.multiple.text=Operatione rückgängig gmacht worde.
dialog.undo.none.title=Undo nöd möglich
dialog.undo.none.text=Keini Operatione könne rückgängig gmacht werde.
-dialog.clearundo.title=Undo-Liste lösche
+dialog.clearundo.title=Undo-Liste löschä
dialog.clearundo.text=Sind Sie sicher, Sie wend die Undo-Liste lösche?\nAlle Undo Infos werdet verlore gah!
-dialog.pointedit.title=Punkt editiere
+dialog.pointedit.title=Punkt editierä
dialog.pointedit.text=Wählet Sie jäden Fäld uus zu editiere, und mitem 'Editiere' Chnopf den Wert ändere
dialog.pointedit.table.field=Fäld
dialog.pointedit.table.value=Wert
dialog.saveexif.overwrite=Files überschriebä
dialog.saveexif.ok1=Es sin
dialog.saveexif.ok2=Fötelis gschriebe worde
+dialog.correlate.title=Fötelis korrelierä
+dialog.correlate.notimestamps=Es hät kei Ziitstämpel inem Track innä, so s'isch nöd möglech die Fötelis zu korrelierä.
+dialog.correlate.nouncorrelatedphotos=Alle Fötelis sin scho korreliert.\nWend Sie trotzdem fortsetzä?
+dialog.correlate.photoselect.intro=Wählet Sie eini vo deren Föteli uus um die Ziitdifferänz zu berächnä
+dialog.correlate.photoselect.photoname=Föteli Name
+dialog.correlate.photoselect.timediff=Ziitdifferänz
+dialog.correlate.photoselect.photolater=Föteli spöter
+dialog.correlate.options.tip=Tipp: Mit mindeschtens einem korrelierten Föteli, die Ziitdifferänz kann automatisch berächnet werdä.
+dialog.correlate.options.intro=Wählet Sie die Optione uus für die Korrelierig
+dialog.correlate.options.offsetpanel=Ziitunterschied
+dialog.correlate.options.offset=Unterschied
+dialog.correlate.options.offset.hours=Schtundä,
+dialog.correlate.options.offset.minutes=Minutä und
+dialog.correlate.options.offset.seconds=Sekundä
+dialog.correlate.options.photolater=Föteli spöter alsem Punkt
+dialog.correlate.options.pointlater=Punkt spöter alsem Föteli
+dialog.correlate.options.limitspanel=Korrelation Gränzä
+dialog.correlate.options.notimelimit=Kei Ziitgränzä
+dialog.correlate.options.timelimit=Ziitgränzä
+dialog.correlate.options.nodistancelimit=Kei Distanzgränzä
+dialog.correlate.options.distancelimit=Distanzgränzä
+dialog.correlate.options.correlate=Korrelierä
+dialog.correlate.alloutsiderange=Alli Fötelis sin uusserhalb vonem Track Ziitruum, so chönne nöd korreliert werdä.\nVersuechet Sie mitenem anderen Offset oder verbindet Sie manuell mindeschtens eis Föteli.
+dialog.correlate.confirmsingle.text=Föteli isch korreliert worde
+dialog.correlate.confirmmultiple.text=Fötelis sin korreliert worde
+dialog.help.help=Bitte lueg na\n http://activityworkshop.net/software/prune/\nfür wiitere Information und Benutzeraaleitige.
dialog.about.title=Ãœber Prune
dialog.about.version=Version
dialog.about.build=Build
dialog.about.credits.code=Prune Code gschriebä vo
dialog.about.credits.exifcode=Exif Code vo
dialog.about.credits.icons=Einigi Ikons vo
+dialog.about.credits.translators=Dolmätscher
dialog.about.credits.translations=Ãœbersetzige mit dr Hilfe vo
dialog.about.credits.devtools=Entwicklungswärkzüüge
dialog.about.credits.othertools=Anderi Wärkzüüge
button.overwrite=Überschriibä
button.moveup=Uufä schiebä
button.movedown=Aba schiebä
-button.deletepoint=Punkt löschä
-button.deleterange=Spanne löschä
button.showlines=Linie aazeigä
button.edit=Editierä
button.exit=Beändä
button.notoall=Nei für alli
button.selectall=Alli selektierä
button.selectnone=Nüüt selektierä
+button.preview=Vorschauä
+button.guessfields=Fälde erratä
# Display components
display.nodata=Kei Date glade worde
details.altitude.to=bis
details.range.climb=Uufstieg
details.range.descent=Abstieg
-details.distanceunits=Distanz Masseinheiten
+details.coordformat=Koordinatenformat
+details.distanceunits=Distanz Masseinheite
display.range.time.secs=S
display.range.time.mins=M
display.range.time.hours=Std
details.photodetails=Details vom Föteli
details.nophoto=Kei föteli selektiert
details.photo.loading=Ladä
+details.photo.connected=Verbundä
# Field names
fieldname.latitude=Breitegrad
fieldname.duration=Ziitlängi
# Measurement units
+units.original=Original
+units.default=Default
units.metres=Meter
units.metres.short=M
units.feet=Fuess
units.degminsec=Grad-Min-Sek
units.degmin=Grad-Min
units.deg=Grad
+units.iso8601=ISO 8601
# Cardinals for 3d plots
cardinal.n=N
undo.reverse=Spanne umdrähie
undo.rearrangewaypoints=Waypoints reorganisierä
undo.connectphoto=Föteli verbindä
+undo.disconnectphoto=Föteli trännä
+undo.correlate=Fötelis korrelierä
# Error messages
-error.save.dialogtitle=Fehler bim Speichere
+error.save.dialogtitle=Fähle bim Speichere
error.save.nodata=Kei Date zum speichere
error.save.failed=Speichere vom File fehlgschlage :
error.saveexif.filenotfound=Föteli File nöd gfunde
error.saveexif.cannotoverwrite1=Föteli File
error.saveexif.cannotoverwrite2=isch nöd schriibbar. Speichere na einer Kopie?
-error.load.dialogtitle=Fehler bim Lade
+error.load.dialogtitle=Fähle bim Lade
error.load.noread=File cha nöd glase werde
-error.load.nopoints=Kei gültigi Information im Datei gfunde
+error.load.nopoints=Kei gültigi Information inem File gfunde
error.load.unknownxml=Unbekanntes xml Format:
-error.load.othererror=Fehler bim Läse:
-error.jpegload.dialogtitle=Fehler bim Lade von Fötelis
+error.load.othererror=Fähle bim Läse:
+error.jpegload.dialogtitle=Fähle bim Lade von Fötelis
error.jpegload.nofilesfound=Kei Dateie gfunde
error.jpegload.nojpegsfound=Kei Jpegs gfunde
error.jpegload.noexiffound=Kei EXIF Information gfunde
error.undofailed.title=Undo isch fehlgschlage worde
error.undofailed.text=Operation kann nöd rückgängig gmacht werde
error.function.noop.title=Funktion hät gar nüüt gmacht
-error.rearrange.noop=Waypoints Reorganisiere hät kein Effäkt gha
+error.rearrange.noop=Waypoints Reorganisierig hät kei Effäkt gha
error.function.notimplemented=Sorry, d'Funktion isch nonig implementiert worde.
error.function.notavailable.title=Funktion nöd verfüegbar
error.function.nojava3d=Sorry, d'Funktion brucht d Java3d Library,\nvo Sun.com odr Blackdown.org erhältlech.
-error.3d.title=Fähler mitm 3d Darstellig
-error.3d=N Fähler isch mitm 3d Darstellig ufgtrete
+error.3d.title=Fähler mitere 3d Darstellig
+error.3d=N Fähler isch mitere 3d Darstellig ufgtrete
menu.file.addphotos=Cargar fotos
menu.file.save=Guardar
menu.file.exportkml=Exportar KML
+menu.file.exportgpx=Exportar GPX
menu.file.exportpov=Exportar POV
menu.file.exit=Salir
menu.edit=Editar
menu.edit.rearrange.nearest=Ir al más próximo
menu.select=Seleccionar
menu.select.all=Seleccionar todo
-menu.select.none=Seleccionar nada
+menu.select.none=No seleccionar nada
menu.select.start=Fijar comienzo
menu.select.end=Fijar final
menu.photo=Foto
menu.photo.saveexif=Guardar Exif
-menu.photo.connect=Connect con punto
+menu.photo.connect=Conectar con punto
+menu.photo.disconnect=Desconectar de punto
+menu.photo.correlate=Correlacionar todas las fotos
menu.photo.delete=Eliminar foto
menu.3d=3-D
menu.3d.show3d=Mostrar en 3-D
menu.map.zoomin=Ampliar zoom
menu.map.zoomout=Reducir zoom
menu.map.zoomfull=Mostrar todo
-menu.map.autopan=Posicionar automático
+menu.map.connect=Conectar puntos de track
+menu.map.autopan=Posicionar automáticamente
# Dialogs
dialog.exit.confirm.title=Salir de Prune
dialog.openappend.title=Agregar a datos existentes
dialog.openappend.text=Agregar estos datos a los datos ya guardados?
dialog.deletepoint.title=Borrar punto
-dialog.deletepoint.deletephoto=Borrar foto tambien?
+dialog.deletepoint.deletephoto=Borrar la foto tambien?
dialog.deletephoto.title=Borrar foto
-dialog.deletephoto.deletepoint=Borrar punto tambien?
+dialog.deletephoto.deletepoint=Borrar el punto tambien?
dialog.deleteduplicates.title=Borrar duplicados
dialog.deleteduplicates.single.text=duplicado eliminado
dialog.deleteduplicates.multi.text=duplicados eliminados
dialog.delimiter.space=Espacio
dialog.delimiter.semicolon=Punto y coma ;
dialog.delimiter.other=Otro
-dialog.openoptions.deliminfo.records=datos, con
+dialog.openoptions.deliminfo.records=datos, con
dialog.openoptions.deliminfo.fields=campos
dialog.openoptions.deliminfo.norecords=Ningun dato
dialog.openoptions.tabledesc=Extraer archivo
dialog.save.table.save=Guardar
dialog.save.headerrow=TÃtulo fila
dialog.save.coordinateunits=Unidades de las coordenadas
-dialog.save.units.original=Original
dialog.save.altitudeunits=Unidades de las altitudes
-dialog.save.oktitle=Guardando archivo
+dialog.save.timestampformat=Format del tiempo
+dialog.save.oktitle=Guardando archivo
dialog.save.ok1=Guardando
dialog.save.ok2=puntos al archivo
dialog.save.overwrite.title=El archivo ya existe
dialog.save.overwrite.text=El archivo ya existe, desea sobreescribirlo?
dialog.exportkml.title=Exportar KML
dialog.exportkml.text=Descripción para los datos
+dialog.exportkml.altitude=Incluir altitudes (para aviación)
dialog.exportkml.kmz=Comprimir al archivo kmz
dialog.exportkml.exportimages=Exportar fotos al kmz
dialog.exportkml.filetype=Archivos KML, KMZ
+dialog.exportgpx.title=Exportar GPX
+dialog.exportgpx.name=Nombre
+dialog.exportgpx.desc=Descripción
+dialog.exportgpx.filetype=Archivos GPX
dialog.exportpov.title=Exportar POV
dialog.exportpov.text=Introdzca los Parametros para exportar
dialog.exportpov.font=Fuente
-dialog.exportpov.camerax=Camera X
-dialog.exportpov.cameray=Camera Y
-dialog.exportpov.cameraz=Camera Z
+dialog.exportpov.camerax=Cámara X
+dialog.exportpov.cameray=Cámara Y
+dialog.exportpov.cameraz=Cámara Z
dialog.exportpov.filetype=Archivos POV
dialog.exportpov.warningtracksize=Este track contiene un gran numero de puntos. Puede ser que Java3D no los pueda visualizar. Está seguro de que desea continuar?
dialog.confirmreversetrack.title=Confirmar inversión
dialog.pointedit.changevalue.title=Editar campo
dialog.pointnameedit.title=Editar nombre de waypoint
dialog.pointnameedit.name=Nombre de waypoint
-dialog.pointnameedit.uppercase=Maysculas
-dialog.pointnameedit.lowercase=minsculas
+dialog.pointnameedit.uppercase=Mayúsculas
+dialog.pointnameedit.lowercase=minúsculas
dialog.pointnameedit.sentencecase=Mezcla
dialog.saveexif.title=Guardar Exif
dialog.saveexif.intro=Seleccione fotos a guardar
-dialog.saveexif.nothingtosave=Coordenadas no han modificados
+dialog.saveexif.nothingtosave=Coordenadas no modificadas, nada que guardar
dialog.saveexif.noexiftool=exiftool program no encontrado. Desea continuar?
-dialog.saveexif.table.photoname=Nombre de foto
-dialog.saveexif.table.status=Status
+dialog.saveexif.table.photoname=Nombre de la foto
+dialog.saveexif.table.status=Estado
dialog.saveexif.table.save=Guardar
-dialog.saveexif.photostatus.connected=Connected
-dialog.saveexif.photostatus.disconnected=Disconnected
-dialog.saveexif.photostatus.modified=Modificado
+dialog.saveexif.photostatus.connected=Conectada
+dialog.saveexif.photostatus.disconnected=Desconectada
+dialog.saveexif.photostatus.modified=Modificada
dialog.saveexif.overwrite=Sobreescribirlar archivos?
dialog.saveexif.ok1=Guardando
dialog.saveexif.ok2=fotos
+dialog.correlate.title=Correlacionar fotos
+dialog.correlate.notimestamps=No hay información de tiempo para los puntos, asà que no hay nada que correlacionar con las fotos.
+dialog.correlate.nouncorrelatedphotos=No hay fotos no correlacionadas.\nEstá seguro de que desea continuar?
+dialog.correlate.photoselect.intro=Seleccione una de estas fotos correlacionadas para usar como margen de tiempo
+dialog.correlate.photoselect.photoname=Nombre de la foto
+dialog.correlate.photoselect.timediff=Diferencia de tiempo
+dialog.correlate.photoselect.photolater=Foto más adelante
+dialog.correlate.options.tip=Sugerencia: Correlacionando al menos una foto manualmente, el margen de tiempo se calcula automáticamente.
+dialog.correlate.options.intro=Seleccionar las opciones para correlación automática
+dialog.correlate.options.offsetpanel=Margen de tiempo
+dialog.correlate.options.offset=Margen
+dialog.correlate.options.offset.hours=horas,
+dialog.correlate.options.offset.minutes=minutos y
+dialog.correlate.options.offset.seconds=segundos
+dialog.correlate.options.photolater=Foto después de punto
+dialog.correlate.options.pointlater=Punto después de foto
+dialog.correlate.options.limitspanel=LÃmites de correlación
+dialog.correlate.options.notimelimit=Sin lÃmite de tiempo
+dialog.correlate.options.timelimit=LÃmite de tiempo
+dialog.correlate.options.nodistancelimit=Sin lÃmite de distancia
+dialog.correlate.options.distancelimit=LÃmite de distancia
+dialog.correlate.options.correlate=Correlacionar
+dialog.correlate.alloutsiderange=Todas las fotos están fuera del margen horario del track, por lo que ninguna puede ser correlada.\nIntente cambiar el margen o correle manualmente al menos una foto.
+dialog.correlate.confirmsingle.text=foto fue correlada
+dialog.correlate.confirmmultiple.text=fotos fueron correladas
+dialog.help.help=Por favor, ver\n http://activityworkshop.net/software/prune/\npara más información y guÃas del usuario.
dialog.about.title=Acerca de Prune
dialog.about.version=Versión
dialog.about.build=Construir
dialog.about.summarytext2=Distribuido bajo el GNU GPL para uso libre y gratuito.<br>Se permite (y se anima) la copia, redistribución y modificación de acuerdo<br>a las condiciones incluidas en el archivo <code>licence.txt</code>.
dialog.about.summarytext3=Por favor, ver <code style="font-weight:bold">http://activityworkshop.net/</code> para más información y guÃas del usuario.
dialog.about.translatedby=Traducción al español realizada por activityworkshop y amigos muy amables!
-dialog.about.systeminfo=Informacion del System
-dialog.about.systeminfo.os=Operating System
+dialog.about.systeminfo=Informacion del sistema
+dialog.about.systeminfo.os=Sistema operativo
dialog.about.systeminfo.java=Java Runtime
-dialog.about.systeminfo.java3d=Java3d installed
-dialog.about.systeminfo.povray=Povray installed
-dialog.about.systeminfo.exiftool=Exiftool installed
+dialog.about.systeminfo.java3d=Java3d instalado
+dialog.about.systeminfo.povray=Povray instalado
+dialog.about.systeminfo.exiftool=Exiftool instalado
dialog.about.yes=Si
dialog.about.no=No
dialog.about.credits=Credits
-dialog.about.credits.code=Prune code written by
-dialog.about.credits.exifcode=Exif code by
-dialog.about.credits.icons=Some icons taken from
-dialog.about.credits.translations=Translations helped by
-dialog.about.credits.devtools=Development tools
-dialog.about.credits.othertools=Other tools
-dialog.about.credits.thanks=Thanks to
+dialog.about.credits.code=El código de Prune fue escrito por
+dialog.about.credits.exifcode=El código Exif por
+dialog.about.credits.icons=Algunos iconos se tomaron de
+dialog.about.credits.translators=Traductores
+dialog.about.credits.translations=Ayuda en la traducción
+dialog.about.credits.devtools=Herramientas de desarrollo
+dialog.about.credits.othertools=Otras herramientas
+dialog.about.credits.thanks=Gracias a
# 3d window
dialog.3d.title=Prune vista 3-D
dialog.3d.altitudecap=Escala de las altitudes
-dialog.3dlines.title=Prune gridlines
-dialog.3dlines.empty=Ninguna información coordenadas encontrada!
-dialog.3dlines.intro=Información de gridlines
+dialog.3dlines.title=CuadrÃcula Prune
+dialog.3dlines.empty=No hay ninguna cuadrÃcula!
+dialog.3dlines.intro=Información de la cuadrÃcula
# Buttons
button.ok=Aceptar
button.overwrite=Sobreescribir
button.moveup=Mover hacia arriba
button.movedown=Mover hacia abajo
-button.deletepoint=Eliminar punto
-button.deleterange=Eliminar rango
-button.showlines=Mostrar gridlines
+button.showlines=Mostrar cuadrÃcula
button.edit=Editar
button.exit=Salir
button.close=Cerrar
button.notoall=No por todo
button.selectall=Seleccionar todo
button.selectnone=Seleccionar nada
+button.preview=Previsualización
+button.guessfields=Adivinar campos
# Display components
display.nodata=Ningún dato cargado
details.altitude.to=hacia
details.range.climb=Ascenso
details.range.descent=Descenso
+details.coordformat=Formato de coordenadas
details.distanceunits=Unidades de distancia
display.range.time.secs=s
display.range.time.mins=m
details.waypointsphotos.waypoints=Waypoints
details.waypointsphotos.photos=Fotos
details.photodetails=Detalles del Foto
-details.nophoto=Ningún foto seleccionado
-details.photo.loading=Cargar
+details.nophoto=Ninguna foto seleccionada
+details.photo.loading=Cargando
+details.photo.connected=Conectada
# Field names
fieldname.latitude=Latitud
fieldname.duration=Duración
# Measurement units
+units.original=Original
+units.default=Por defecto
units.metres=Metros
units.metres.short=m
units.feet=Pies
units.degminsec=Gra-min-seg
units.degmin=Gra-min
units.deg=Grados
+units.iso8601=ISO 8601
# Cardinals for 3d plots
cardinal.n=N
undo.deleteduplicates=eliminar duplicados
undo.reverse=invertir rango
undo.rearrangewaypoints=reordenar waypoints
-undo.connectphoto=connectar foto
+undo.connectphoto=conectar foto
+undo.disconnectphoto=desconectar foto
+undo.correlate=correlacionar fotos
# Error messages
error.save.dialogtitle=Fallo al guardar datos
error.load.dialogtitle=Fallo al cargar datos
error.load.noread=No se puede leer el fichero
error.load.nopoints=Ninguna información coordenadas encontrada
-error.load.unknownxml=Unrecognised xml format:
+error.load.unknownxml=Formato xml no reconocido:
error.load.othererror=Fallo al cargar datos:
error.jpegload.dialogtitle=Error cargando fotos
error.jpegload.nofilesfound=Ningún archivo encontrado
menu.file.addphotos=Ouvrir photos
menu.file.save=Enregistrer
menu.file.exportkml=Exporter au KML
+menu.file.exportgpx=Exporter au GPX
menu.file.exportpov=Exporter au POV
menu.file.exit=Quitter
menu.edit=Édition
-menu.edit.undo=Undo
+menu.edit.undo=Annuler
menu.edit.clearundo=Purger undo liste
menu.edit.editpoint=Editer point
menu.edit.editwaypointname=Editer nom du waypoint
menu.select=Sélectionner
menu.select.all=Tous sélectionner
menu.select.none=Rien sélectionner
-menu.select.start=Set range start
-menu.select.end=Set range end
+menu.select.start=Set range début
+menu.select.end=Set range fin
menu.photo=Photo
menu.photo.saveexif=Enregistrer à Exif
-menu.photo.connect=Connect to point
+menu.photo.connect=Relier au point
+menu.photo.disconnect=Disconnect from point
+menu.photo.correlate=Corréler tous les photos
menu.photo.delete=Remove photo
menu.3d=Trois-D
menu.3d.show3d=Montrer en Trois-D
menu.map.zoomin=Zoom avant
menu.map.zoomout=Zoom arrière
menu.map.zoomfull=Zoom to full scale
+menu.map.connect=Connect track points
menu.map.autopan=Pan automatique
# Dialogs
dialog.exit.confirm.title=Terminer Prune
-dialog.exit.confirm.text=Le data a été modifié. Souhaitez-vous terminer Prune sans enregistrement?
-dialog.openappend.title=Append to existing data
-dialog.openappend.text=Append this data to the data already loaded?
-dialog.deletepoint.title=Delete Point
-dialog.deletepoint.deletephoto=Delete photo attached to this point?
-dialog.deletephoto.title=Delete Photo
-dialog.deletephoto.deletepoint=Delete point attached to this photo?
-dialog.deleteduplicates.title=Delete Duplicates
-dialog.deleteduplicates.single.text=duplicate was deleted
-dialog.deleteduplicates.multi.text=duplicates were deleted
+dialog.exit.confirm.text=Les données ont été modifié. Souhaitez-vous terminer Prune sans enregistrement?
+dialog.openappend.title=Append to existing données
+dialog.openappend.text=Append this to the données already loaded?
+dialog.deletepoint.title=Effacer point
+dialog.deletepoint.deletephoto=Effacer photo attached to this point?
+dialog.deletephoto.title=Effacer photo
+dialog.deletephoto.deletepoint=Effacer point attached to this photo?
+dialog.deleteduplicates.title=Effacer duplicates
+dialog.deleteduplicates.single.text=duplicate a été effacé
+dialog.deleteduplicates.multi.text=duplicates ont été effacés
dialog.deleteduplicates.nonefound=No duplicates found
-dialog.compresstrack.title=Compress Track
+dialog.compresstrack.title=Comprimer track
dialog.compresstrack.parameter.text=Parameter for compression (lower number = more compression)
dialog.compresstrack.text=Track compressed
-dialog.compresstrack.single.text=data point was removed
-dialog.compresstrack.multi.text=data points were removed
-dialog.compresstrack.nonefound=No data points could be removed
-dialog.openoptions.title=Open options
+dialog.compresstrack.single.text=point a été effacé
+dialog.compresstrack.multi.text=points ont été effacés
+dialog.compresstrack.nonefound=Pas de données ont été effacés
+dialog.openoptions.title=Ouvrir options
dialog.openoptions.filesnippet=Extract of fichier
-dialog.load.table.field=Field
-dialog.load.table.datatype=Data Type
+dialog.load.table.field=Champ
+dialog.load.table.datatype=Typ des données
dialog.load.table.description=Description
dialog.delimiter.label=Séparateur de texte
dialog.delimiter.comma=Virgule ,
dialog.jpegload.photosadded=photos were added
dialog.saveoptions.title=Save fichier
dialog.save.fieldstosave=Fields to save
-dialog.save.table.field=Field
-dialog.save.table.hasdata=Has data
+dialog.save.table.field=Champ
+dialog.save.table.hasdata=A information
dialog.save.table.save=Enregistrer
dialog.save.headerrow=Output header row
dialog.save.coordinateunits=Unités des coordonnées
-dialog.save.units.original=Original
dialog.save.altitudeunits=Unités de altitude
+dialog.save.timestampformat=Format de timestamp
dialog.save.oktitle=File saved
dialog.save.ok1=Successfully saved
dialog.save.ok2=points to fichier
dialog.save.overwrite.title=File already exists
dialog.save.overwrite.text=This fichier already exists. Are you sure you want to overwrite the fichier?
dialog.exportkml.title=Exporter au KML
-dialog.exportkml.text=Title for the data
-dialog.exportkml.kmz=Compress to make kmz fichier
-dialog.exportkml.exportimages=Export image thumbnails to kmz
+dialog.exportkml.text=Titre pour le data
+dialog.exportkml.altitude=Include altitudes (pour aviation)
+dialog.exportkml.kmz=Comprimer à kmz fichier
+dialog.exportkml.exportimages=Export image thumbnails à kmz
dialog.exportkml.filetype=Classeur KML, KMZ
+dialog.exportgpx.title=Exporter au GPX
+dialog.exportgpx.name=Nom
+dialog.exportgpx.desc=Légende
+dialog.exportgpx.filetype=Classeur GPX
dialog.exportpov.title=Exporter au POV
dialog.exportpov.text=Please enter the parameters for the POV export
dialog.exportpov.font=Police
dialog.saveexif.intro=Select the photos to save using the checkboxes
dialog.saveexif.nothingtosave=Coordinate data is unchanged, nothing to save
dialog.saveexif.noexiftool=No exiftool program could be found. Continue?
-dialog.saveexif.table.photoname=Photo name
+dialog.saveexif.table.photoname=Nom de photo
dialog.saveexif.table.status=Status
-dialog.saveexif.table.save=Save
+dialog.saveexif.table.save=Enregistrer
dialog.saveexif.photostatus.connected=Connected
dialog.saveexif.photostatus.disconnected=Disconnected
dialog.saveexif.photostatus.modified=Modified
dialog.saveexif.overwrite=Overwrite fichiers
dialog.saveexif.ok1=Saved
dialog.saveexif.ok2=photo fichiers
+dialog.correlate.title=Correlate photos
+dialog.correlate.notimestamps=Les points n'ont pas de timestamps, donc ce n'est pas possible de correler.
+dialog.correlate.nouncorrelatedphotos=There are no uncorrelated photos.\nAre you sure you want to continue?
+dialog.correlate.photoselect.intro=Select one of these correlated photos to use as the time offset
+dialog.correlate.photoselect.photoname=Nom de photo
+dialog.correlate.photoselect.timediff=Difference de temps
+dialog.correlate.photoselect.photolater=Photo plus tard
+dialog.correlate.options.tip=Tip: By manually correlating at least one photo, the time offset can be calculated for you.
+dialog.correlate.options.intro=Select the options for automatic correlation
+dialog.correlate.options.offsetpanel=Offset de temps
+dialog.correlate.options.offset=Offset
+dialog.correlate.options.offset.hours=heures,
+dialog.correlate.options.offset.minutes=minutes et
+dialog.correlate.options.offset.seconds=secondes
+dialog.correlate.options.photolater=Photo later than point
+dialog.correlate.options.pointlater=Point later than photo
+dialog.correlate.options.limitspanel=Correlation limits
+dialog.correlate.options.notimelimit=No time limit
+dialog.correlate.options.timelimit=Time limit
+dialog.correlate.options.nodistancelimit=No distance limit
+dialog.correlate.options.distancelimit=Distance limit
+dialog.correlate.options.correlate=Correlate
+dialog.correlate.alloutsiderange=All photos are outside the time range of the track, so none can be correlated.\nTry changing the offset or manually correlating at least one photo.
+dialog.correlate.confirmsingle.text=photo was correlated
+dialog.correlate.confirmmultiple.text=photos were correlated
+dialog.help.help=Consultez la page\n http://activityworkshop.net/software/prune/\npour de plus détails et user guides.
dialog.about.title=À propos de Prune
dialog.about.version=Version
dialog.about.build=Build
dialog.about.systeminfo=Info de Systeme
dialog.about.systeminfo.os=Operating Systeme
dialog.about.systeminfo.java=Java Runtime
-dialog.about.systeminfo.java3d=Java3d installed
-dialog.about.systeminfo.povray=Povray installed
-dialog.about.systeminfo.exiftool=Exiftool installed
+dialog.about.systeminfo.java3d=Java3d installé
+dialog.about.systeminfo.povray=Povray installé
+dialog.about.systeminfo.exiftool=Exiftool installé
dialog.about.yes=Oui
dialog.about.no=Non
-dialog.about.credits=Credits
-dialog.about.credits.code=Prune code written by
-dialog.about.credits.exifcode=Exif code by
+dialog.about.credits=Crédits
+dialog.about.credits.code=Prune code écrit par
+dialog.about.credits.exifcode=Exif code par
dialog.about.credits.icons=Some icons taken from
-dialog.about.credits.translations=Translations helped by
-dialog.about.credits.devtools=Development tools
-dialog.about.credits.othertools=Other tools
-dialog.about.credits.thanks=Thanks to
+dialog.about.credits.translators=Interprète
+dialog.about.credits.translations=Traduction avec l'aide de
+dialog.about.credits.devtools=Outils de développement
+dialog.about.credits.othertools=Autre outils
+dialog.about.credits.thanks=Merci Ã
# 3d window
dialog.3d.title=Vue Trois-d de Prune
button.next=Prochain
button.finish=Fini
button.cancel=Annuler
-button.overwrite=Overwrite
+button.overwrite=Écraser
button.moveup=Move up
button.movedown=Move down
-button.deletepoint=Delete point
-button.deleterange=Delete range
-button.showlines=Show lines
+button.showlines=Montrer lignes
button.edit=Éditer
button.exit=Terminer
button.close=Fermer
button.notoall=Non pour tous
button.selectall=Sélecter tous
button.selectnone=Sélecter rien
+button.preview=Preview
+button.guessfields=Guess fields
# Display components
-display.nodata=No data loaded
+display.nodata=Pas de data loaded
display.noaltitudes=Track data does not include altitudes
-details.trackdetails=Track details
-details.notrack=No track loaded
+details.trackdetails=Détails de track
+details.notrack=Pas de track loaded
details.track.points=Points
-details.track.file=File
-details.track.numfiles=Number of fichiers
-details.pointdetails=Point details
+details.track.file=Fichier
+details.track.numfiles=Nombre de fichiers
+details.pointdetails=Détails de point
details.index.selected=Index
details.index.of=de
-details.nopointselection=No point selected
+details.nopointselection=Pas de point choisis
details.photofile=Photo fichier
-details.norangeselection=No range selected
+details.norangeselection=No range choisis
details.rangedetails=Range details
-details.range.selected=Selected
+details.range.selected=Choisis
details.range.to=Ã
details.altitude.to=Ã
details.range.climb=Montée
details.range.descent=Descente
+details.coordformat=Coordinate format
details.distanceunits=Unités de distance
display.range.time.secs=s
display.range.time.mins=m
display.range.time.days=j
details.waypointsphotos.waypoints=Waypoints
details.waypointsphotos.photos=Photos
-details.photodetails=Photo details
-details.nophoto=Pas de Photo
-details.photo.loading=Loading
+details.photodetails=Détails de photo
+details.nophoto=Pas de photo
+details.photo.loading=Charger
+details.photo.connected=Connected
# Field names
fieldname.latitude=Latitude
fieldname.duration=Durée
# Measurement units
+units.original=Original
+units.default=Default
units.metres=mètres
units.metres.short=m
units.feet=pieds
units.degminsec=Deg-min-sec
units.degmin=Deg-min
units.deg=Degrés
+units.iso8601=ISO 8601
# Cardinals for 3d plots
cardinal.n=N
undo.reverse=reverse range
undo.rearrangewaypoints=rearrange waypoints
undo.connectphoto=connect photo
+undo.disconnectphoto=disconnect photo
+undo.correlate=correlate photos
# Error messages
error.save.dialogtitle=Error saving data
--- /dev/null
+# Text entries for the Prune application
+# Polish entries as extra
+
+# Menu entries
+menu.file=Plik
+menu.file.open=Otw\u00F3rz
+menu.file.addphotos=Dodaj zdj\u0119cia
+menu.file.save=Zapisz
+menu.file.exportkml=Eksportuj jako KML
+menu.file.exportgpx=Eksportuj jako GPX
+menu.file.exportpov=Eksportuj jako POV
+menu.file.exit=Zako\u0144cz
+menu.edit=Edycja
+menu.edit.undo=Cofnij
+menu.edit.clearundo=Wyczy\u015B\u0107 list\u0119 zmian
+menu.edit.editpoint=Edytuj punkt
+menu.edit.editwaypointname=Zmie\u0144 nazw\u0119 punktu po\u015Bredniego
+menu.edit.deletepoint=Usu\u0144 punkt
+menu.edit.deleterange=Usu\u0144 zakres
+menu.edit.deleteduplicates=Usu\u0144 duplikaty
+menu.edit.compress=Skompresuj scie\u017Ck\u0119
+menu.edit.interpolate=Interpoluj punkty
+menu.edit.reverse=Odwr\u00F3\u0107 zakres
+menu.edit.rearrange=Zmie\u0144 kolejno\u015B\u0107 punkt\u00F3w po\u015Brednich
+menu.edit.rearrange.start=Wszystkie na pocz\u0105tek \u015Bcie\u017Cki
+menu.edit.rearrange.end=Wszystkie na koniec \u015Bcie\u017Cki
+menu.edit.rearrange.nearest=Do najbli\u017Cszego punktu
+menu.select=Zakres
+menu.select.all=Zaznacz wszystko
+menu.select.none=Usu\u0144 zaznaczenie
+menu.select.start=Zaznacz pocz\u0105tek
+menu.select.end=Zaznacz koniec
+menu.photo=Zdj\u0119cie
+menu.photo.saveexif=Zapisz Exif
+menu.photo.connect=Przy\u0142\u0105cz do punktu
+menu.photo.disconnect=Od\u0142\u0105cz od punktu
+menu.photo.correlate=Skoreluj wszystkie zdj\u0119cia
+menu.photo.delete=Usu\u0144 zdj\u0119cie
+menu.3d=Operacje 3D
+menu.3d.show3d=Poka\u017C model
+menu.help=Pomoc
+menu.help.about=Prune - Informacje
+# Popup menu for map
+menu.map.zoomin=Powi\u0119ksz
+menu.map.zoomout=Zmniejsz
+menu.map.zoomfull=Dostosuj powi\u0119kszenie
+menu.map.connect=Connect track punkty
+menu.map.autopan=Autopan
+
+# Dialogs
+dialog.exit.confirm.title=Zako\u0144cz Prune
+dialog.exit.confirm.text=Your data is not saved. Are you sure you want to exit?
+dialog.openappend.title=Append to existing data
+dialog.openappend.text=Append this data to the data already loaded?
+dialog.deletepoint.title=Usu\u0144 punkt
+dialog.deletepoint.deletephoto=Usu\u0144 zdj\u0119cie attached to this punkt?
+dialog.deletephoto.title=Usu\u0144 zdj\u0119cie
+dialog.deletephoto.deletepoint=Usu\u0144 punkt attached to this zdj\u0119cie?
+dialog.deleteduplicates.title=Usu\u0144 Duplicates
+dialog.deleteduplicates.single.text=duplicate was deleted
+dialog.deleteduplicates.multi.text=duplicates were deleted
+dialog.deleteduplicates.nonefound=Brak duplikaty found
+dialog.compresstrack.title=Skompresuj scie\u017Ck\u0119
+dialog.compresstrack.parameter.text=Parameter for compression (lower number = more compression)
+dialog.compresstrack.text=Track compressed
+dialog.compresstrack.single.text=data punkt was removed
+dialog.compresstrack.multi.text=data punkty were removed
+dialog.compresstrack.nonefound=No data punkty could be removed
+dialog.openoptions.title=Otw\u00F3rz opcje
+dialog.openoptions.filesnippet=Extract of plik
+dialog.load.table.field=Pole
+dialog.load.table.datatype=Data Type
+dialog.load.table.description=Opis
+dialog.delimiter.label=Pole separator
+dialog.delimiter.comma=Przecinek ,
+dialog.delimiter.tab=Tabulator
+dialog.delimiter.space=Spacja
+dialog.delimiter.semicolon=\u015Arednik ;
+dialog.delimiter.other=Inne
+dialog.openoptions.deliminfo.records=records, with
+dialog.openoptions.deliminfo.fields=pola
+dialog.openoptions.deliminfo.norecords=No records
+dialog.openoptions.tabledesc=Extract of plik
+dialog.openoptions.altitudeunits=Wysoko\u015B\u0107 units
+dialog.jpegload.subdirectories=Include subdirectories
+dialog.jpegload.loadjpegswithoutcoords=Include zdj\u0119cia without coordinates
+dialog.jpegload.progress.title=Loading zdj\u0119cia
+dialog.jpegload.progress=Please wait while the zdj\u0119cia are searched
+dialog.jpegload.title=Loaded zdj\u0119cia
+dialog.jpegload.photoadded=zdj\u0119cie was added
+dialog.jpegload.photosadded=zdj\u0119cia were added
+dialog.saveoptions.title=Zapisz plik
+dialog.save.fieldstosave=Pola to save
+dialog.save.table.field=Pole
+dialog.save.table.hasdata=Has data
+dialog.save.table.save=Zapisz
+dialog.save.headerrow=Output header row
+dialog.save.coordinateunits=Wsp\u00f3\u0142rz\u0119dne units
+dialog.save.altitudeunits=Wysoko\u015B\u0107 units
+dialog.save.timestampformat=Timestamp format
+dialog.save.oktitle=Plik saved
+dialog.save.ok1=Successfully saved
+dialog.save.ok2=punkty to plik
+dialog.save.overwrite.title=Plik ju\u017C istnieje
+dialog.save.overwrite.text=This plik already exists. Are you sure you want to overwrite the plik?
+dialog.exportkml.title=Eksportuj KML
+dialog.exportkml.text=Tytu\u0142 for the data
+dialog.exportkml.altitude=Include altitudes (for aviation)
+dialog.exportkml.kmz=Compress to make kmz plik
+dialog.exportkml.exportimages=Eksportuj image thumbnails to kmz
+dialog.exportkml.filetype=KML, KMZ pliki
+dialog.exportgpx.title=Eksportuj GPX
+dialog.exportgpx.name=Nazwa
+dialog.exportgpx.desc=Opis
+dialog.exportgpx.filetype=GPX pliki
+dialog.exportpov.title=Eksportuj POV
+dialog.exportpov.text=Please enter the parameters for the POV export
+dialog.exportpov.font=Czcionka
+dialog.exportpov.camerax=Camera X
+dialog.exportpov.cameray=Camera Y
+dialog.exportpov.cameraz=Camera Z
+dialog.exportpov.filetype=POV pliki
+dialog.exportpov.warningtracksize=This track has a large number of punkty, which Java3D might not be able to display.\nCzy chcesz kontynuowa\u0107?
+dialog.confirmreversetrack.title=Confirm reversal
+dialog.confirmreversetrack.text=This track contains timestamp information, which will be out of sequence after a reversal.\nAre you sure you want to reverse this section?
+dialog.interpolate.title=Interpoluj punkty
+dialog.interpolate.parameter.text=Number of punkty to insert between selected punkty
+dialog.undo.title=Cofnij action(s)
+dialog.undo.pretext=Please select the action(s) to undo
+dialog.confirmundo.title=Operation(s) undone
+dialog.confirmundo.single.text=operation undone.
+dialog.confirmundo.multiple.text=operations undone.
+dialog.undo.none.title=Cannot undo
+dialog.undo.none.text=No operations to undo!
+dialog.clearundo.title=Wyczy\u015B\u0107 list\u0119 zmian
+dialog.clearundo.text=Are you sure you want to clear the undo list?\nAll undo information will be lost!
+dialog.pointedit.title=Edytuj punkt
+dialog.pointedit.text=Select each field to edit and use the 'Edit' button to change the value
+dialog.pointedit.table.field=Pole
+dialog.pointedit.table.value=Value
+dialog.pointedit.table.changed=Zmieniony
+dialog.pointedit.changevalue.text=Enter the new value for this field
+dialog.pointedit.changevalue.title=Edytuj field
+dialog.pointnameedit.title=Zmie\u0144 nazw\u0119 punktu po\u015Bredniego
+dialog.pointnameedit.name=Waypoint nazwa
+dialog.pointnameedit.uppercase=UPPER case
+dialog.pointnameedit.lowercase=lower case
+dialog.pointnameedit.sentencecase=Sentence case
+dialog.saveexif.title=Zapisz Exif
+dialog.saveexif.intro=Select the zdj\u0119cia to save using the checkboxes
+dialog.saveexif.nothingtosave=Coordinate data is unchanged, nothing to save
+dialog.saveexif.noexiftool=No exiftool program could be found. Continue?
+dialog.saveexif.table.photoname=Nazwa zdj\u0119cie
+dialog.saveexif.table.status=Status
+dialog.saveexif.table.save=Zapisz
+dialog.saveexif.photostatus.connected=Connected
+dialog.saveexif.photostatus.disconnected=Disconnected
+dialog.saveexif.photostatus.modified=Modified
+dialog.saveexif.overwrite=Overwrite pliki
+dialog.saveexif.ok1=Saved
+dialog.saveexif.ok2=zdj\u0119cia pliki
+dialog.correlate.title=Skoreluj zdj\u0119cie
+dialog.correlate.notimestamps=There are no timestamps in the data punkty, so there is nothing to correlate with the zdj\u0119cia.
+dialog.correlate.nouncorrelatedphotos=There are no uncorrelated zdj\u0119cia.\nAre you sure you want to continue?
+dialog.correlate.photoselect.intro=Select one of these correlated zdj\u0119cia to use as the time offset
+dialog.correlate.photoselect.photoname=Nazwa zdj\u0119cie
+dialog.correlate.photoselect.timediff=Time difference
+dialog.correlate.photoselect.photolater=Zdj\u0119cie later
+dialog.correlate.options.tip=Tip: By manually correlating at least one zdj\u0119cie, the time offset can be calculated for you.
+dialog.correlate.options.intro=Select the options for automatic correlation
+dialog.correlate.options.offsetpanel=Time offset
+dialog.correlate.options.offset=Offset
+dialog.correlate.options.offset.hours=hours,
+dialog.correlate.options.offset.minutes=minuty i
+dialog.correlate.options.offset.seconds=seconds
+dialog.correlate.options.photolater=Zdj\u0119cie po punkt
+dialog.correlate.options.pointlater=Punkt po zdj\u0119cie
+dialog.correlate.options.limitspanel=Correlation limits
+dialog.correlate.options.notimelimit=No time limit
+dialog.correlate.options.timelimit=Time limit
+dialog.correlate.options.nodistancelimit=No distance limit
+dialog.correlate.options.distancelimit=Distance limit
+dialog.correlate.options.correlate=Correlate
+dialog.correlate.alloutsiderange=All zdj\u0119cia are outside the time range of the track, so none can be correlated.\nTry changing the offset or manually correlating at least one zdj\u0119cie.
+dialog.correlate.confirmsingle.text=zdj\u0119cie was correlated
+dialog.correlate.confirmmultiple.text=zdj\u0119cia were correlated
+dialog.help.help=Please see\n http://activityworkshop.net/software/prune/\nfor more information and user guides.
+dialog.about.title=Prune Informacje
+dialog.about.version=Wersja
+dialog.about.build=Build
+dialog.about.summarytext1=Prune is a program for loading, displaying and editing data from GPS receivers.
+dialog.about.summarytext2=It is released under the Gnu GPL for free, open, worldwide use and enhancement.<br>Copying, redistribution and modification are permitted and encouraged<br>according to the conditions in the included <code>license.txt</code> file.
+dialog.about.summarytext3=Please see <code style="font-weight:bold">http://activityworkshop.net/</code> for more information and user guides.
+dialog.about.translatedby=Tekst po polsku by Piotr.
+dialog.about.systeminfo=System info
+dialog.about.systeminfo.os=Operating System
+dialog.about.systeminfo.java=Java Runtime
+dialog.about.systeminfo.java3d=Java3d zainstalowana
+dialog.about.systeminfo.povray=Povray zainstalowana
+dialog.about.systeminfo.exiftool=Exiftool zainstalowana
+dialog.about.yes=Tak
+dialog.about.no=Nie
+dialog.about.credits=Credits
+dialog.about.credits.code=Prune code written by
+dialog.about.credits.exifcode=Exif code by
+dialog.about.credits.icons=Some icons taken from
+dialog.about.credits.translators=Translators
+dialog.about.credits.translations=T\u0142umaczenie helped by
+dialog.about.credits.devtools=Development tools
+dialog.about.credits.othertools=Other tools
+dialog.about.credits.thanks=Dzi\u0119kuje to
+
+# 3d window
+dialog.3d.title=Prune tr\u00f3jwymiarowa model
+dialog.3d.altitudecap=Minimum altitude range
+dialog.3dlines.title=Prune gridlines
+dialog.3dlines.empty=No gridlines to display!
+dialog.3dlines.intro=These are the gridlines for the three-d view
+
+# Buttons
+button.ok=OK
+button.back=Poprzedni
+button.next=Nast\u0119pny
+button.finish=Finish
+button.cancel=Anuluj
+button.overwrite=Overwrite
+button.moveup=Do g\u00F3ry
+button.movedown=Move down
+button.showlines=Show lines
+button.edit=Edycja
+button.exit=Zako\u0144cz
+button.close=Zamknij
+button.continue=Continue
+button.yes=Tak
+button.no=Nie
+button.yestoall=Tak to all
+button.notoall=Nie to all
+button.selectall=Select all
+button.selectnone=Select none
+button.preview=Preview
+button.guessfields=Guess fields
+
+# Display components
+display.nodata=No data loaded
+display.noaltitudes=Track data does not include altitudes
+details.trackdetails=Track szczeg\u00F3\u0142y
+details.notrack=No track loaded
+details.track.points=Punkty
+details.track.file=Plik
+details.track.numfiles=Number ze pliki
+details.pointdetails=Punkt szczeg\u00F3\u0142y
+details.index.selected=Index
+details.index.of=of
+details.nopointselection=No punkt selected
+details.photofile=Plik zdj\u0119cie
+details.norangeselection=No range selected
+details.rangedetails=Range szczeg\u00F3\u0142y
+details.range.selected=Selected
+details.range.to=to
+details.altitude.to=to
+details.range.climb=Climb
+details.range.descent=Descent
+details.coordformat=Wsp\u00f3\u0142rz\u0119dne format
+details.distanceunits=Distance units
+display.range.time.secs=s
+display.range.time.mins=m
+display.range.time.hours=h
+display.range.time.days=d
+details.waypointsphotos.waypoints=Waypoints
+details.waypointsphotos.photos=Zdj\u0119cia
+details.photodetails=Zdj\u0119cie szczeg\u00F3\u0142y
+details.nophoto=No zdj\u0119cie selected
+details.photo.loading=Wczytywanie
+details.photo.connected=Connected
+
+# Field names
+fieldname.latitude=Szeroko\u015B\u0107
+fieldname.longitude=D\u0142ugo\u015B\u0107
+fieldname.altitude=Wysoko\u015B\u0107
+fieldname.timestamp=Timestamp
+fieldname.waypointname=Nazwa
+fieldname.waypointtype=Type
+fieldname.newsegment=Segment
+fieldname.custom=U\u017Cytkownika
+fieldname.prefix=Pole
+fieldname.distance=Distance
+fieldname.duration=Duration
+
+# Measurement units
+units.original=Oryginalny
+units.default=Default
+units.metres=Metres
+units.metres.short=m
+units.feet=Feet
+units.feet.short=ft
+units.kilometres=Kilometres
+units.kilometres.short=km
+units.miles=Miles
+units.miles.short=mi
+units.degminsec=Deg-min-sek
+units.degmin=Deg-min
+units.deg=Degrees
+units.iso8601=ISO 8601
+
+# Cardinals for 3d plots
+cardinal.n=N
+cardinal.s=S
+cardinal.e=E
+cardinal.w=W
+
+# Undo operations
+undo.load=load data
+undo.loadphotos=load zdj\u0119cia
+undo.editpoint=edycja punkt
+undo.deletepoint=usu\u0144 punkt
+undo.deletephoto=remove zdj\u0119cie
+undo.deleterange=usu\u0144 range
+undo.compress=compress track
+undo.insert=insert punkty
+undo.deleteduplicates=usu\u0144 duplicates
+undo.reverse=reverse range
+undo.rearrangewaypoints=rearrange waypoints
+undo.connectphoto=connect zdj\u0119cie
+undo.disconnectphoto=disconnect zdj\u0119cie
+undo.correlate=correlate zdj\u0119cia
+
+# Error messages
+error.save.dialogtitle=Error saving data
+error.save.nodata=No data to save
+error.save.failed=Failed to save the data to plik:
+error.saveexif.filenotfound=Failed to find zdj\u0119cie plik
+error.saveexif.cannotoverwrite1=Zdj\u0119cie plik
+error.saveexif.cannotoverwrite2=is read-only and can't be overwritten. Write to copy?
+error.load.dialogtitle=B\u0142\u0105d loading data
+error.load.noread=Cannot read plik
+error.load.nopoints=No coordinate information found in the plik
+error.load.unknownxml=Nieznany xml format:
+error.load.othererror=B\u0142\u0105d reading plik:
+error.jpegload.dialogtitle=B\u0142\u0105d loading zdj\u0119cia
+error.jpegload.nofilesfound=No pliki found
+error.jpegload.nojpegsfound=No jpeg pliki found
+error.jpegload.noexiffound=No EXIF information found
+error.jpegload.nogpsfound=No GPS information found
+error.undofailed.title=Undo failed
+error.undofailed.text=Failed to undo operation
+error.function.noop.title=Function had no effect
+error.rearrange.noop=Rearranging waypoints had no effect
+error.function.notimplemented=Sorry, this function has not yet been implemented.
+error.function.notavailable.title=Function not available
+error.function.nojava3d=This function requires the Java3d library,\navailable from Sun.com or Blackdown.org.
+error.3d.title=B\u0142\u0105d in 3d display
+error.3d=A b\u0142\u0105d occurred with the 3d display
_delimiter = inChar;
}
+ /** @return the delimiter character */
public char getDelimiter()
{
return _delimiter;
}
+ /** @return the max number of fields */
public int getMaxFields()
{
return _maxFields;
}
- public void updateMaxFields(int inNumields)
+ /** @param inNumFields number of fields */
+ public void updateMaxFields(int inNumFields)
{
- if (inNumields > _maxFields)
- _maxFields = inNumields;
+ if (inNumFields > _maxFields)
+ _maxFields = inNumFields;
}
-
+ /** @return the number of records */
public int getNumRecords()
{
return _numRecords;
}
+
+ /** Increment the number of records */
public void incrementNumRecords()
{
_numRecords++;
}
+ /** @return the number of times this delimiter has won */
public int getNumWinningRecords()
{
return _numWinningRecords;
}
+
+ /** Increment the number of times this delimiter has won */
public void incrementNumWinningRecords()
{
_numWinningRecords++;
}
+ /** @return String for debug */
public String toString()
{
return "(delim:" + _delimiter + " fields:" + _maxFields + ", records:" + _numRecords + ")";
--- /dev/null
+package tim.prune.load;
+
+import tim.prune.I18nManager;
+import tim.prune.data.Field;
+import tim.prune.data.Latitude;
+import tim.prune.data.Longitude;
+import tim.prune.data.Timestamp;
+
+/**
+ * Class to try to match data with field names,
+ * using a variety of guessing techniques
+ */
+public abstract class FieldGuesser
+{
+ /**
+ * Try to guess whether the given line is a header line or data
+ * @param inValues array of values from first non-blank line of file
+ * @return true if it looks like a header row, false if it looks like data
+ */
+ private static boolean isHeaderRow(String[] inValues)
+ {
+ // Loop over values looking for a Latitude value
+ if (inValues != null)
+ {
+ for (int v=0; v<inValues.length; v++)
+ {
+ Latitude lat = new Latitude(inValues[v]);
+ if (lat.isValid()) {return false;}
+ }
+ }
+ // No valid Latitude value found so presume header
+ return true;
+ }
+
+
+ /**
+ * Try to guess the fields for the given values from the file
+ * @param inValues array of values from first non-blank line of file
+ * @return array of fields which hopefully match
+ */
+ public static Field[] guessFields(String[] inValues)
+ {
+ // Guess whether it's a header line or not
+ boolean isHeader = isHeaderRow(inValues);
+ // make array of Fields
+ int numFields = inValues.length;
+ Field[] fields = new Field[numFields];
+ // Loop over fields to try to guess the main ones
+ for (int f=0; f<numFields; f++)
+ {
+ if (inValues[f] != null) {
+ String value = inValues[f].trim();
+ // check for latitude
+ if (!checkArrayHasField(fields, Field.LATITUDE) && fieldLooksLikeLatitude(value, isHeader))
+ {
+ fields[f] = Field.LATITUDE;
+ continue;
+ }
+ // check for longitude
+ if (!checkArrayHasField(fields, Field.LONGITUDE) && fieldLooksLikeLongitude(value, isHeader))
+ {
+ fields[f] = Field.LONGITUDE;
+ continue;
+ }
+ // check for altitude
+ if (!checkArrayHasField(fields, Field.ALTITUDE) && fieldLooksLikeAltitude(value, isHeader))
+ {
+ fields[f] = Field.ALTITUDE;
+ continue;
+ }
+ // check for waypoint name
+ if (!checkArrayHasField(fields, Field.WAYPT_NAME) && fieldLooksLikeName(value, isHeader))
+ {
+ fields[f] = Field.WAYPT_NAME;
+ continue;
+ }
+ // check for timestamp
+ if (!checkArrayHasField(fields, Field.TIMESTAMP) && fieldLooksLikeTimestamp(value, isHeader))
+ {
+ fields[f] = Field.TIMESTAMP;
+ continue;
+ }
+ }
+ }
+ // Fill in the rest of the fields using just custom fields
+ // Could try to guess other fields (waypoint type, segment) or unguessed altitude, name, but keep simple for now
+ String customPrefix = I18nManager.getText("fieldname.prefix") + " ";
+ int customFieldNum = 0;
+ for (int f=0; f<numFields; f++) {
+ if (fields[f] == null)
+ {
+ // Make sure lat and long are filled in if not already
+ if (!checkArrayHasField(fields, Field.LATITUDE)) {
+ fields[f] = Field.LATITUDE;
+ }
+ else if (!checkArrayHasField(fields, Field.LONGITUDE)) {
+ fields[f] = Field.LONGITUDE;
+ }
+ else {
+ customFieldNum++;
+ fields[f] = new Field(customPrefix + (customFieldNum));
+ }
+ }
+ }
+ // Do a final check to make sure lat and long are in there
+ if (!checkArrayHasField(fields, Field.LATITUDE)) {
+ fields[0] = Field.LATITUDE;
+ }
+ else if (!checkArrayHasField(fields, Field.LONGITUDE)) {
+ fields[1] = Field.LONGITUDE;
+ }
+ return fields;
+ }
+
+
+ /**
+ * Check whether the given field array has the specified field
+ * @param inFields
+ * @param inCheckField
+ * @return true if Field is contained within the array
+ */
+ private static boolean checkArrayHasField(Field[] inFields, Field inCheckField)
+ {
+ for (int f=0; f<inFields.length; f++)
+ {
+ if (inFields[f] != null && inFields[f].equals(inCheckField)) {
+ return true;
+ }
+ }
+ // not found
+ return false;
+ }
+
+
+ /**
+ * Check whether the given String looks like a Latitude value
+ * @param inValue value from file
+ * @param inIsHeader true if this is a header line, false for data
+ * @return true if it could be latitude
+ */
+ private static boolean fieldLooksLikeLatitude(String inValue, boolean inIsHeader)
+ {
+ if (inValue == null || inValue.equals("")) {return false;}
+ if (inIsHeader)
+ {
+ // This is a header line so look for english or local text
+ String upperValue = inValue.toUpperCase();
+ return (upperValue.equals("LATITUDE")
+ || upperValue.equals(I18nManager.getText("fieldname.latitude").toUpperCase()));
+ }
+ else
+ {
+ // Note this will also catch longitudes too
+ Latitude lat = new Latitude(inValue);
+ return lat.isValid();
+ }
+ }
+
+ /**
+ * Check whether the given String looks like a Longitude value
+ * @param inValue value from file
+ * @param inIsHeader true if this is a header line, false for data
+ * @return true if it could be longitude
+ */
+ private static boolean fieldLooksLikeLongitude(String inValue, boolean inIsHeader)
+ {
+ if (inValue == null || inValue.equals("")) {return false;}
+ if (inIsHeader)
+ {
+ // This is a header line so look for english or local text
+ String upperValue = inValue.toUpperCase();
+ return (upperValue.equals("LONGITUDE")
+ || upperValue.equals(I18nManager.getText("fieldname.longitude").toUpperCase()));
+ }
+ else
+ {
+ // Note this will also catch latitudes too
+ Longitude lon = new Longitude(inValue);
+ return lon.isValid();
+ }
+ }
+
+ /**
+ * Check whether the given String looks like an Altitude value
+ * @param inValue value from file
+ * @param inIsHeader true if this is a header line, false for data
+ * @return true if it could be altitude
+ */
+ private static boolean fieldLooksLikeAltitude(String inValue, boolean inIsHeader)
+ {
+ if (inValue == null || inValue.equals("")) {return false;}
+ if (inIsHeader)
+ {
+ // This is a header line so look for english or local text
+ String upperValue = inValue.toUpperCase();
+ return (upperValue.equals("ALTITUDE")
+ || upperValue.equals("ALT")
+ || upperValue.equals(I18nManager.getText("fieldname.altitude").toUpperCase()));
+ }
+ else
+ {
+ // Look for a number less than 100000
+ try
+ {
+ int intValue = Integer.parseInt(inValue);
+ return (intValue > 0 && intValue < 100000);
+ }
+ catch (NumberFormatException nfe) {}
+ return false;
+ }
+ }
+
+
+ /**
+ * Check whether the given String looks like a waypoint name
+ * @param inValue value from file
+ * @param inIsHeader true if this is a header line, false for data
+ * @return true if it could be a name
+ */
+ private static boolean fieldLooksLikeName(String inValue, boolean inIsHeader)
+ {
+ if (inValue == null || inValue.equals("")) {return false;}
+ if (inIsHeader)
+ {
+ // This is a header line so look for english or local text
+ String upperValue = inValue.toUpperCase();
+ return (upperValue.equals("NAME")
+ || upperValue.equals("LABEL")
+ || upperValue.equals(I18nManager.getText("fieldname.waypointname").toUpperCase()));
+ }
+ else
+ {
+ // Look for at least two letters in it
+ int numLetters = 0;
+ for (int i=0; i<inValue.length(); i++)
+ {
+ char currChar = inValue.charAt(i);
+ if (Character.isLetter(currChar)) {
+ numLetters++;
+ }
+ // Not interested if it contains ":" or "."
+ if (currChar == ':' || currChar == '.') {return false;}
+ }
+ return numLetters >= 2;
+ }
+ }
+
+ /**
+ * Check whether the given String looks like a timestamp
+ * @param inValue value from file
+ * @param inIsHeader true if this is a header line, false for data
+ * @return true if it could be a timestamp
+ */
+ private static boolean fieldLooksLikeTimestamp(String inValue, boolean inIsHeader)
+ {
+ if (inValue == null || inValue.equals("")) {return false;}
+ if (inIsHeader)
+ {
+ String upperValue = inValue.toUpperCase();
+ // This is a header line so look for english or local text
+ return (upperValue.equals("TIMESTAMP")
+ || upperValue.equals("TIME")
+ || upperValue.equals(I18nManager.getText("fieldname.timestamp").toUpperCase()));
+ }
+ else
+ {
+ // must be at least 7 characters long
+ if (inValue.length() < 7) {return false;}
+ Timestamp stamp = new Timestamp(inValue);
+ return stamp.isValid();
+ }
+ }
+}
/**
- * Get the column count
+ * @return the column count
*/
public int getColumnCount()
{
/**
- * Get the name of the column
+ * @param inColNum column number
+ * @return name of the column
*/
public String getColumnName(int inColNum)
{
/**
- * Get the row count
+ * @return the row count
*/
public int getRowCount()
{
/**
- * Get the value of the specified cell
+ * @param inRowIndex row index
+ * @param inColumnIndex column index
+ * @return the value of the specified cell
*/
- public Object getValueAt(int rowIndex, int columnIndex)
+ public Object getValueAt(int inRowIndex, int inColumnIndex)
{
if (_fieldArray == null) return "";
- if (columnIndex == 0) return ("" + (rowIndex+1));
- Field field = _fieldArray[rowIndex];
- if (columnIndex == 1)
+ if (inColumnIndex == 0) return ("" + (inRowIndex+1));
+ Field field = _fieldArray[inRowIndex];
+ if (inColumnIndex == 1)
{
// Field name - take name from built-in fields
if (field.isBuiltIn())
/**
* Make sure only second and third columns are editable
+ * @param inRowIndex row index
+ * @param inColumnIndex column index
+ * @return true if cell editable
*/
- public boolean isCellEditable(int rowIndex, int columnIndex)
+ public boolean isCellEditable(int inRowIndex, int inColumnIndex)
{
- if (columnIndex <= 1)
- return (columnIndex == 1);
+ if (inColumnIndex <= 1)
+ return (inColumnIndex == 1);
// Column is 2 so only edit non-builtin field names
- Field field = _fieldArray[rowIndex];
+ Field field = _fieldArray[inRowIndex];
return !field.isBuiltIn();
}
/**
* React to edits to the table data
+ * @param inValue value to set
+ * @param inRowIndex row index
+ * @param inColumnIndex column index
*/
- public void setValueAt(Object aValue, int rowIndex, int columnIndex)
+ public void setValueAt(Object inValue, int inRowIndex, int inColumnIndex)
{
- super.setValueAt(aValue, rowIndex, columnIndex);
- if (columnIndex == 1)
+ super.setValueAt(inValue, inRowIndex, inColumnIndex);
+ if (inColumnIndex == 1)
{
- Field field = _fieldArray[rowIndex];
- if (!field.getName().equals(aValue.toString()))
+ Field field = _fieldArray[inRowIndex];
+ if (!field.getName().equals(inValue.toString()))
{
- manageFieldChange(rowIndex, aValue.toString());
+ manageFieldChange(inRowIndex, inValue.toString());
}
}
- else if (columnIndex == 2)
+ else if (inColumnIndex == 2)
{
// change description if it's custom
- Field field = _fieldArray[rowIndex];
+ Field field = _fieldArray[inRowIndex];
if (!field.isBuiltIn())
- field.setName(aValue.toString());
+ field.setName(inValue.toString());
}
}
/**
* Get the top section of the file for preview
- * @param inSize number of lines to extract
+ * @param inNumRows number of lines to extract
+ * @param inMaxWidth max length of Strings (longer ones will be chopped)
* @return String array containing non-blank lines from the file
*/
public String[] getSnippet(int inNumRows, int inMaxWidth)
package tim.prune.load;
-import tim.prune.I18nManager;
-import tim.prune.data.Field;
-
/**
* Class responsible for splitting the file contents into an array
* based on the selected delimiter character
private int _numRows = 0;
private int _numColumns = 0;
private boolean[] _columnStates = null;
+ private String[] _firstFullRow = null;
+
/**
* Constructor
*/
public String[][] splitFieldData(char inDelim)
{
+ _firstFullRow = null;
if (_cacher == null) return null;
String[] contents = _cacher.getContents();
if (contents == null || contents.length == 0) return null;
if (splitLine != null && splitLine.length > maxFields)
{
maxFields = splitLine.length;
+ _firstFullRow = splitLine;
}
}
}
return _numColumns;
}
+ /**
+ * @return the fields in the first full row
+ */
+ public String[] getFirstFullRow()
+ {
+ return _firstFullRow;
+ }
+
/**
* Check if the specified column of the data is blank
// Should probably trap out of range values
return !_columnStates[inColumnNum];
}
-
-
- /**
- * @return a Field array to use as defaults for the data
- */
- public Field[] makeDefaultFields()
- {
- Field[] fields = null;
- if (_numColumns > 0)
- {
- fields = new Field[_numColumns];
- try
- {
- fields[0] = Field.LATITUDE;
- fields[1] = Field.LONGITUDE;
- fields[2] = Field.ALTITUDE;
- fields[3] = Field.WAYPT_NAME;
- fields[4] = Field.WAYPT_TYPE;
- String customPrefix = I18nManager.getText("fieldname.prefix") + " ";
- for (int i=5;; i++)
- {
- fields[i] = new Field(customPrefix + (i+1));
- }
- }
- catch (ArrayIndexOutOfBoundsException finished)
- {
- // Finished populating array
- }
- }
- else
- fields = new Field[0];
- return fields;
- }
}
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
-import java.util.ArrayList;
+import java.util.TreeSet;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
private JProgressBar _progressBar = null;
private int[] _fileCounts = null;
private boolean _cancelled = false;
- private ArrayList _photos = null;
+ private TreeSet _photos = null;
/**
{
// Initialise arrays, errors, summaries
_fileCounts = new int[4]; // files, jpegs, exifs, gps
- _photos = new ArrayList();
+ _photos = new TreeSet(new PhotoSorter());
File[] files = _fileChooser.getSelectedFiles();
// Loop recursively over selected files/directories to count files
int numFiles = countFileList(files, true, _subdirCheckbox.isSelected());
{_fileCounts[2]++;} // exif found
if (jpegData.isValid())
{
- if (jpegData.getDatestamp() != null && jpegData.getTimestamp() != null)
+ if (jpegData.getGpsDatestamp() != null && jpegData.getGpsTimestamp() != null)
{
- photo.setTimestamp(createTimestamp(jpegData.getDatestamp(), jpegData.getTimestamp()));
+ photo.setTimestamp(createTimestamp(jpegData.getGpsDatestamp(), jpegData.getGpsTimestamp()));
}
// Make DataPoint and attach to Photo
DataPoint point = createDataPoint(jpegData);
photo.setOriginalStatus(PhotoStatus.TAGGED);
_fileCounts[3]++;
}
+ // Use exif timestamp if gps timestamp not available
+ if (photo.getTimestamp() == null && jpegData.getOriginalTimestamp() != null)
+ {
+ photo.setTimestamp(createTimestamp(jpegData.getOriginalTimestamp()));
+ }
+ photo.setExifThumbnail(jpegData.getThumbnailImage());
}
catch (JpegException jpe) { // don't list errors, just count them
}
}
+ /**
+ * Use the given String value to create a timestamp
+ * @param inStamp timestamp from exif
+ * @return Timestamp object corresponding to input
+ */
+ private static Timestamp createTimestamp(String inStamp)
+ {
+ Timestamp stamp = null;
+ try
+ {
+ stamp = new Timestamp(Integer.parseInt(inStamp.substring(0, 4)),
+ Integer.parseInt(inStamp.substring(5, 7)),
+ Integer.parseInt(inStamp.substring(8, 10)),
+ Integer.parseInt(inStamp.substring(11, 13)),
+ Integer.parseInt(inStamp.substring(14, 16)),
+ Integer.parseInt(inStamp.substring(17)));
+ }
+ catch (NumberFormatException nfe) {}
+ return stamp;
+ }
+
+
/**
* Check whether to accept the given filename
* @param inName name of file
+++ /dev/null
-package tim.prune.load;
-
-import tim.prune.data.Photo;
-import tim.prune.data.PhotoList;
-
-/**
- * This class starts a new thread to preload image sizes
- * TODO: # Cache small image thumbnails too?
- */
-public class PhotoMeasurer implements Runnable
-{
- /** PhotoList to loop through */
- private PhotoList _photoList = null;
-
-
- /**
- * Constructor
- * @param inPhotoList photo list to loop through
- */
- public PhotoMeasurer(PhotoList inPhotoList)
- {
- _photoList = inPhotoList;
- }
-
-
- /**
- * Start off the process to measure the photo sizes
- */
- public void measurePhotos()
- {
- // check if any photos in list
- if (_photoList != null && _photoList.getNumPhotos() > 0)
- {
- // start new thread
- new Thread(this).start();
- }
- }
-
-
- /**
- * Run method called in new thread
- */
- public void run()
- {
- try
- {
- // loop over all photos in list
- for (int i=0; i<_photoList.getNumPhotos(); i++)
- {
- Photo photo = _photoList.getPhoto(i);
- if (photo != null)
- {
- // call get size method which will calculate it if necessary
- photo.getSize();
- }
- }
- }
- catch (ArrayIndexOutOfBoundsException obe) {} // ignore, must have been changed by other thread
- }
-}
--- /dev/null
+package tim.prune.load;
+
+import java.io.File;
+import java.util.Comparator;
+
+import tim.prune.data.Photo;
+
+/**
+ * Class to sort photos by name
+ */
+public class PhotoSorter implements Comparator
+{
+
+ /**
+ * Compare two photos
+ * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
+ */
+ public int compare(Object o1, Object o2)
+ {
+ File file1 = ((Photo) o1).getFile();
+ File file2 = ((Photo) o2).getFile();
+ int nameComp = file1.getName().compareTo(file2.getName());
+ if (nameComp == 0)
+ {
+ // names same, maybe in different directories
+ return file1.getAbsolutePath().compareTo(file2.getAbsolutePath());
+ }
+ // names different
+ return nameComp;
+ }
+
+}
/**
- * Open the selected file and show the GUI dialog
- * to select load options
+ * Open the selected file and show the GUI dialog to select load options
+ * @param inFile file to open
*/
public void openFile(File inFile)
{
}
});
innerPanel3.add(_moveDownButton);
- innerPanel3.add(Box.createVerticalStrut(70));
+ innerPanel3.add(Box.createVerticalStrut(60));
+ JButton guessButton = new JButton(I18nManager.getText("button.guessfields"));
+ guessButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ _lastSelectedFields = null;
+ prepareSecondPanel();
+ }
+ });
+ innerPanel3.add(guessButton);
innerPanel2.add(innerPanel3, BorderLayout.EAST);
secondCard.add(innerPanel2, BorderLayout.CENTER);
FileSplitter splitter = new FileSplitter(_fileCacher);
// Check info makes sense - num fields > 0, num records > 0
// set "Finished" button to disabled if not ok
- // TODO: Work out if there are header rows or not, save?
- // Try to match header rows with fields
- // Try to match data with fields
// Add data to GUI elements
- Object[][] tableData = splitter.splitFieldData(info.getDelimiter());
+ String[][] tableData = splitter.splitFieldData(info.getDelimiter());
// possible to ignore blank columns here
_currentDelimiter = info.getDelimiter();
_fileExtractTableModel.updateData(tableData);
// Check number of fields and use last ones if count matches
Field[] startFieldArray = null;
if (_lastSelectedFields != null && splitter.getNumColumns() == _lastSelectedFields.length)
+ {
startFieldArray = _lastSelectedFields;
+ }
else
- startFieldArray = splitter.makeDefaultFields();
+ {
+ // Take first full row of file and use it to guess fields
+ startFieldArray = FieldGuesser.guessFields(splitter.getFirstFullRow());
+ }
+
_fieldTableModel.updateData(startFieldArray);
_fieldTable.setModel(_fieldTableModel);
// add dropdowns to second column
*/
public class GpxHandler extends XmlHandler
{
+ private boolean _insideWaypoint = false;
private boolean _insideName = false;
private boolean _insideElevation = false;
private boolean _insideTime = false;
* @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
*/
public void startElement(String uri, String localName, String qName,
- Attributes attributes) throws SAXException
+ Attributes attributes) throws SAXException
{
// Read the parameters for waypoints and track points
if (qName.equalsIgnoreCase("wpt") || qName.equalsIgnoreCase("trkpt"))
{
+ _insideWaypoint = qName.equalsIgnoreCase("wpt");
int numAttributes = attributes.getLength();
for (int i=0; i<numAttributes; i++)
{
* @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
*/
public void endElement(String uri, String localName, String qName)
- throws SAXException
+ throws SAXException
{
if (qName.equalsIgnoreCase("wpt") || qName.equalsIgnoreCase("trkpt"))
{
* @see org.xml.sax.ContentHandler#characters(char[], int, int)
*/
public void characters(char[] ch, int start, int length)
- throws SAXException
+ throws SAXException
{
String value = new String(ch, start, length);
- if (_insideName) {_name = checkCharacters(_name, value);}
+ if (_insideName && _insideWaypoint) {_name = checkCharacters(_name, value);}
else if (_insideElevation) {_elevation = checkCharacters(_elevation, value);}
else if (_insideTime) {_time = checkCharacters(_time, value);}
super.characters(ch, start, length);
/**
- * Open the selected file and show the GUI dialog
- * to select load options
+ * Open the selected file and show the GUI dialog to select load options
+ * @param inFile File to open
*/
public void openFile(File inFile)
{
* @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
*/
public void startElement(String uri, String localName, String qName,
- Attributes attributes) throws SAXException
+ Attributes attributes) throws SAXException
{
// Check for "kml" or "gpx" tags
if (_handler == null)
* @see org.xml.sax.ContentHandler#characters(char[], int, int)
*/
public void characters(char[] ch, int start, int length)
- throws SAXException
+ throws SAXException
{
if (_handler != null)
{
* @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
*/
public void endElement(String uri, String localName, String qName)
- throws SAXException
+ throws SAXException
{
if (_handler != null)
{
-Prune version 3
+Prune version 4
===============
-Prune is an application for viewing, editing and managing coordinate data from GPS systems.
+Prune is an application for viewing, editing and managing coordinate data from GPS systems,
+including format conversion and photo correlation.
Prune is copyright activityworkshop.net and distributed under the terms of the Gnu GPL version 2.
You may freely use the software, and may help others to freely use it too. For further information
=======
To run Prune from the jar file, simply call it from a command prompt or shell:
- java -jar prune_03.jar
+ java -jar prune_04.jar
If the jar file is saved in a different directory, you will need to include the path.
Depending on your system settings, you may be able to click or double-click on the jar file
in a file manager window to execute it. A shortcut, menu item, desktop icon or other link
can of course be made should you wish.
+To specify a language other than the default, use an additional parameter, eg:
+ java -jar prune_04.jar --lang=DE
-Updates since version 2
-=======================
+
+New with version 4
+==================
+
+The following features were added since version 3:
+ - Automatic correlation of photos with points based on timestamps
+ - Manual disconnection of photos from points
+ - Reading of photo thumbnails from exif data (speeds up photo loading)
+ - Export to GPX format
+ - KML and KMZ export now includes altitudes option for airborne tracks
+ - Track points in map can be connected by lines
+ - On loading a text file, fields are now guessed according to data or column headings
+ - Polish language
+
+New with version 3
+==================
The following features were added since version 2:
- Loading of GPX and KML files
- Saving of coordinates in exif data of jpegs
- Exporting to KMZ format including thumbnails of photos
- Four-panel layout with toolbar
+ - French language
-Updates since version 1
-=======================
+New with version 2
+==================
The following features were added since version 1:
- Display of data in 3d view using Java3D library
- Export of 3d model to POV format for rendering by povray
- Point edit dialog, waypoint name edit dialog
- Waypoint list
+ - Spanish language
Further information and updates
* Save exif information to all photos in the list
* whose coordinate information has changed since loading
* @param inPhotoList list of photos to save
+ * @return true if saved
*/
public boolean saveExifInformation(PhotoList inPhotoList)
{
/**
* Constructor
+ * @param inField Field object
+ * @param inData true if Field contains data which can be saved
*/
public FieldInfo(Field inField, boolean inData)
{
/**
* Constructor giving list size
+ * @param inSize number of fields
*/
public FieldSelectionTableModel(int inSize)
{
}
else if (inColumnIndex == 1)
{
- return new Boolean(_info[inRowIndex].hasData());
+ return Boolean.valueOf(_info[inRowIndex].hasData());
}
- return new Boolean(_info[inRowIndex].isSelected());
+ return Boolean.valueOf(_info[inRowIndex].isSelected());
}
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 javax.swing.ListSelectionModel;
+import javax.swing.table.TableModel;
import tim.prune.App;
import tim.prune.I18nManager;
import tim.prune.data.DataPoint;
import tim.prune.data.Field;
import tim.prune.data.FieldList;
+import tim.prune.data.Timestamp;
import tim.prune.data.Track;
import tim.prune.load.OneCharDocument;
private JTable _table = null;
private FieldSelectionTableModel _model = null;
private JButton _moveUpButton = null, _moveDownButton = null;
+ private UpDownToggler _toggler = null;
private JRadioButton[] _delimiterRadios = null;
private JTextField _otherDelimiterText = null;
private JCheckBox _headerRowCheckbox = null;
private JRadioButton[] _coordUnitsRadios = null;
private JRadioButton[] _altitudeUnitsRadios = null;
+ private JRadioButton[] _timestampUnitsRadios = null;
private static final int[] FORMAT_COORDS = {Coordinate.FORMAT_NONE, Coordinate.FORMAT_DEG_MIN_SEC,
Coordinate.FORMAT_DEG_MIN, Coordinate.FORMAT_DEG};
private static final int[] FORMAT_ALTS = {Altitude.FORMAT_NONE, Altitude.FORMAT_METRES, Altitude.FORMAT_FEET};
+ private static final int[] FORMAT_TIMES = {Timestamp.FORMAT_ORIGINAL, Timestamp.FORMAT_LOCALE, Timestamp.FORMAT_ISO_8601};
/**
*/
public void showDialog(char inDefaultDelimiter)
{
- _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.saveoptions.title"), true);
- _dialog.setLocationRelativeTo(_parentFrame);
+ if (_dialog == null)
+ {
+ _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.saveoptions.title"), true);
+ _dialog.setLocationRelativeTo(_parentFrame);
+ _dialog.getContentPane().add(makeDialogComponents());
+ _dialog.pack();
+ }
// Check field list
FieldList fieldList = _track.getFieldList();
int numFields = fieldList.getNumFields();
FieldInfo info = new FieldInfo(field, _track.hasData(field));
_model.addFieldInfo(info, i);
}
- _dialog.getContentPane().add(makeDialogComponents(_model, inDefaultDelimiter));
- _dialog.pack();
+ // Initialise dialog and show it
+ initDialog(_model, inDefaultDelimiter);
_dialog.show();
}
/**
* Make the dialog components
- * @param inTableModel table model for fields
- * @param inDelimiter default delimiter character
* @return the GUI components for the save dialog
*/
- private Component makeDialogComponents(FieldSelectionTableModel inTableModel, char inDelimiter)
+ private Component makeDialogComponents()
{
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
firstCard.setLayout(new BoxLayout(firstCard, BoxLayout.Y_AXIS));
JPanel tablePanel = new JPanel();
tablePanel.setLayout(new BorderLayout());
- _table = new JTable(inTableModel);
+ _table = new JTable();
_table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
- tablePanel.add(_table.getTableHeader(), BorderLayout.NORTH);
- tablePanel.add(_table, BorderLayout.CENTER);
+ // Enclose table in a scrollpane to prevent other components getting lost
+ JScrollPane scrollPane = new JScrollPane(_table);
+ _table.setPreferredScrollableViewportSize(new Dimension(300, 150));
+ tablePanel.add(scrollPane, BorderLayout.CENTER);
// Make a panel to hold the table and up/down buttons
JPanel fieldsPanel = new JPanel();
updownPanel.add(_moveDownButton);
fieldsPanel.add(updownPanel, BorderLayout.EAST);
// enable/disable buttons based on table row selection
- _table.getSelectionModel().addListSelectionListener(
- new UpDownToggler(_moveUpButton, _moveDownButton, inTableModel.getRowCount())
- );
+ _toggler = new UpDownToggler(_moveUpButton, _moveDownButton);
+ _table.getSelectionModel().addListSelectionListener(_toggler);
// Add fields panel and the delimiter panel to first card in pack
JLabel saveOptionsLabel = new JLabel(I18nManager.getText("dialog.save.fieldstosave"));
{
delimGroup.add(_delimiterRadios[i]);
}
- // choose last-used delimiter as default
- switch (inDelimiter)
- {
- case ',' : _delimiterRadios[0].setSelected(true); break;
- case '\t' : _delimiterRadios[1].setSelected(true); break;
- case ';' : _delimiterRadios[2].setSelected(true); break;
- case ' ' : _delimiterRadios[3].setSelected(true); break;
- default : _delimiterRadios[4].setSelected(true);
- _otherDelimiterText.setText("" + inDelimiter);
- }
delimsPanel.add(otherPanel);
firstCard.add(delimsPanel);
coordsUnitsPanel.setBorder(BorderFactory.createEtchedBorder());
coordsUnitsPanel.setLayout(new GridLayout(0, 2));
_coordUnitsRadios = new JRadioButton[4];
- _coordUnitsRadios[0] = new JRadioButton(I18nManager.getText("dialog.save.units.original"));
+ _coordUnitsRadios[0] = new JRadioButton(I18nManager.getText("units.original"));
_coordUnitsRadios[1] = new JRadioButton(I18nManager.getText("units.degminsec"));
_coordUnitsRadios[2] = new JRadioButton(I18nManager.getText("units.degmin"));
_coordUnitsRadios[3] = new JRadioButton(I18nManager.getText("units.deg"));
coordsUnitsPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
secondCardHolder.add(coordsUnitsPanel);
secondCardHolder.add(Box.createRigidArea(new Dimension(0,10)));
+ // altitude units
JLabel altUnitsLabel = new JLabel(I18nManager.getText("dialog.save.altitudeunits"));
altUnitsLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT);
secondCardHolder.add(altUnitsLabel);
altUnitsPanel.setBorder(BorderFactory.createEtchedBorder());
altUnitsPanel.setLayout(new GridLayout(0, 2));
_altitudeUnitsRadios = new JRadioButton[3];
- _altitudeUnitsRadios[0] = new JRadioButton(I18nManager.getText("dialog.save.units.original"));
+ _altitudeUnitsRadios[0] = new JRadioButton(I18nManager.getText("units.original"));
_altitudeUnitsRadios[1] = new JRadioButton(I18nManager.getText("units.metres"));
_altitudeUnitsRadios[2] = new JRadioButton(I18nManager.getText("units.feet"));
ButtonGroup altGroup = new ButtonGroup();
}
altUnitsPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
secondCardHolder.add(altUnitsPanel);
- // TODO: selection of format of timestamps
+ secondCardHolder.add(Box.createRigidArea(new Dimension(0,10)));
+ // Selection of format of timestamps
+ JLabel timestampLabel = new JLabel(I18nManager.getText("dialog.save.timestampformat"));
+ timestampLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT);
+ secondCardHolder.add(timestampLabel);
+ JPanel timestampPanel = new JPanel();
+ timestampPanel.setBorder(BorderFactory.createEtchedBorder());
+ timestampPanel.setLayout(new GridLayout(0, 2));
+ _timestampUnitsRadios = new JRadioButton[3];
+ _timestampUnitsRadios[0] = new JRadioButton(I18nManager.getText("units.original"));
+ _timestampUnitsRadios[1] = new JRadioButton(I18nManager.getText("units.default"));
+ _timestampUnitsRadios[2] = new JRadioButton(I18nManager.getText("units.iso8601"));
+ ButtonGroup timeGroup = new ButtonGroup();
+ for (int i=0; i<3; i++)
+ {
+ timeGroup.add(_timestampUnitsRadios[i]);
+ timestampPanel.add(_timestampUnitsRadios[i]);
+ _timestampUnitsRadios[i].setSelected(i==0);
+ }
+ timestampPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
+ secondCardHolder.add(timestampPanel);
secondCard.add(secondCardHolder, BorderLayout.NORTH);
_cards.add(secondCard, "card2");
_backButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e)
{
- CardLayout cl = (CardLayout)(_cards.getLayout());
+ CardLayout cl = (CardLayout) _cards.getLayout();
cl.previous(_cards);
_backButton.setEnabled(false);
_nextButton.setEnabled(true);
_nextButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e)
{
- CardLayout cl = (CardLayout)(_cards.getLayout());
+ CardLayout cl = (CardLayout) _cards.getLayout();
cl.next(_cards);
_backButton.setEnabled(true);
_nextButton.setEnabled(false);
return panel;
}
+ /**
+ * Initialize the dialog with the given details
+ * @param inModel table model
+ * @param inDefaultDelimiter default delimiter character
+ */
+ private void initDialog(TableModel inModel, char inDefaultDelimiter)
+ {
+ // set table model
+ _table.setModel(inModel);
+ // reset toggler
+ _toggler.setListSize(inModel.getRowCount());
+ // choose last-used delimiter as default
+ switch (inDefaultDelimiter)
+ {
+ case ',' : _delimiterRadios[0].setSelected(true); break;
+ case '\t' : _delimiterRadios[1].setSelected(true); break;
+ case ';' : _delimiterRadios[2].setSelected(true); break;
+ case ' ' : _delimiterRadios[3].setSelected(true); break;
+ default : _delimiterRadios[4].setSelected(true);
+ _otherDelimiterText.setText("" + inDefaultDelimiter);
+ }
+ // set card and enable buttons
+ CardLayout cl = (CardLayout) _cards.getLayout();
+ cl.first(_cards);
+ _nextButton.setEnabled(true);
+ _backButton.setEnabled(false);
+ }
+
/**
* Save the track to file with the chosen options
altitudeFormat = FORMAT_ALTS[i];
}
}
+ // Get timestamp formats
+ int timestampFormat = Timestamp.FORMAT_ORIGINAL;
+ for (int i=0; i<_timestampUnitsRadios.length; i++)
+ {
+ if (_timestampUnitsRadios[i].isSelected())
+ {
+ timestampFormat = FORMAT_TIMES[i];
+ }
+ }
// Check if file exists, and confirm overwrite if necessary
Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
{
try
{
- buffer.append(point.getTimestamp().getText());
+ if (timestampFormat == Timestamp.FORMAT_ORIGINAL) {
+ // output original string
+ buffer.append(point.getFieldValue(Field.TIMESTAMP));
+ }
+ else {
+ // format value accordingly
+ buffer.append(point.getTimestamp().getText(timestampFormat));
+ }
}
catch (NullPointerException npe) {}
}
--- /dev/null
+package tim.prune.save;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JFileChooser;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.filechooser.FileFilter;
+
+import tim.prune.GpsPruner;
+import tim.prune.I18nManager;
+import tim.prune.data.Altitude;
+import tim.prune.data.Coordinate;
+import tim.prune.data.DataPoint;
+import tim.prune.data.Timestamp;
+import tim.prune.data.Track;
+import tim.prune.data.TrackInfo;
+
+/**
+ * Class to export track information
+ * into a specified Gpx file
+ */
+public class GpxExporter implements Runnable
+{
+ private JFrame _parentFrame = null;
+ private Track _track = null;
+ private JDialog _dialog = null;
+ private JTextField _nameField = null;
+ private JTextField _descriptionField = null;
+ private JFileChooser _fileChooser = null;
+ private File _exportFile = null;
+
+ /** version number of Gpx */
+ private static final String GPX_VERSION_NUMBER = "1.1";
+ /** this program name */
+ private static final String GPX_CREATOR = "Prune v" + GpsPruner.VERSION_NUMBER + " activityworkshop.net";
+
+
+ /**
+ * Constructor giving frame and track
+ * @param inParentFrame parent frame
+ * @param inTrackInfo track info object to save
+ */
+ public GpxExporter(JFrame inParentFrame, TrackInfo inTrackInfo)
+ {
+ _parentFrame = inParentFrame;
+ _track = inTrackInfo.getTrack();
+ }
+
+
+ /**
+ * Show the dialog to select options and export file
+ */
+ public void showDialog()
+ {
+ // Make dialog window
+ if (_dialog == null)
+ {
+ _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.exportgpx.title"), true);
+ _dialog.setLocationRelativeTo(_parentFrame);
+ _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+ _dialog.getContentPane().add(makeDialogComponents());
+ _dialog.pack();
+ }
+ _dialog.show();
+ }
+
+
+ /**
+ * Create dialog components
+ * @return Panel containing all gui elements in dialog
+ */
+ private Component makeDialogComponents()
+ {
+ JPanel dialogPanel = new JPanel();
+ dialogPanel.setLayout(new BorderLayout());
+ JPanel mainPanel = new JPanel();
+ mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
+ // Make a central panel with the text boxes
+ JPanel descPanel = new JPanel();
+ descPanel.setLayout(new GridLayout(2, 2));
+ descPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.name")));
+ _nameField = new JTextField(10);
+ descPanel.add(_nameField);
+ descPanel.add(new JLabel(I18nManager.getText("dialog.exportgpx.desc")));
+ _descriptionField = new JTextField(10);
+ descPanel.add(_descriptionField);
+ mainPanel.add(descPanel);
+ dialogPanel.add(mainPanel, BorderLayout.CENTER);
+
+ // button panel at bottom
+ JPanel buttonPanel = new JPanel();
+ buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
+ JButton okButton = new JButton(I18nManager.getText("button.ok"));
+ ActionListener okListener = new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ startExport();
+ }
+ };
+ okButton.addActionListener(okListener);
+ _descriptionField.addActionListener(okListener);
+ 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);
+ dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
+ return dialogPanel;
+ }
+
+
+ /**
+ * Start the export process based on the input parameters
+ */
+ private void startExport()
+ {
+ // OK pressed, so choose output file
+ if (_fileChooser == null)
+ {_fileChooser = new JFileChooser();}
+ _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
+ _fileChooser.setFileFilter(new FileFilter() {
+ public boolean accept(File f)
+ {
+ return (f != null && (f.isDirectory()
+ || f.getName().toLowerCase().endsWith(".gpx")));
+ }
+ public String getDescription()
+ {
+ return I18nManager.getText("dialog.exportgpx.filetype");
+ }
+ });
+ _fileChooser.setAcceptAllFileFilterUsed(false);
+ // Allow choose again if an existing file is selected
+ boolean chooseAgain = false;
+ do
+ {
+ chooseAgain = false;
+ if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
+ {
+ // OK pressed and file chosen
+ File file = _fileChooser.getSelectedFile();
+ // Check file extension
+ if (!file.getName().toLowerCase().endsWith(".gpx"))
+ {
+ file = new File(file.getAbsolutePath() + ".gpx");
+ }
+ // Check if file exists and if necessary prompt for overwrite
+ Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
+ if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
+ I18nManager.getText("dialog.save.overwrite.text"),
+ I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
+ JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
+ == JOptionPane.YES_OPTION)
+ {
+ // New file or overwrite confirmed, so initiate export in separate thread
+ _exportFile = file;
+ new Thread(this).start();
+ }
+ else
+ {
+ chooseAgain = true;
+ }
+ }
+ } while (chooseAgain);
+ }
+
+
+ /**
+ * Run method for controlling separate thread for exporting
+ */
+ public void run()
+ {
+ OutputStreamWriter writer = null;
+ try
+ {
+ // normal writing to file
+ writer = new OutputStreamWriter(new FileOutputStream(_exportFile));
+ // write file
+ int numPoints = exportData(writer);
+
+ // close file
+ writer.close();
+ JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.ok1")
+ + " " + numPoints + " " + I18nManager.getText("dialog.save.ok2")
+ + " " + _exportFile.getAbsolutePath(),
+ I18nManager.getText("dialog.save.oktitle"), JOptionPane.INFORMATION_MESSAGE);
+ // export successful so need to close dialog and return
+ _dialog.dispose();
+ return;
+ }
+ catch (IOException ioe)
+ {
+ // System.out.println("Exception: " + ioe.getClass().getName() + " - " + ioe.getMessage());
+ try {
+ if (writer != null) writer.close();
+ }
+ catch (IOException ioe2) {}
+ JOptionPane.showMessageDialog(_parentFrame,
+ I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
+ I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
+ }
+ // if not returned already, export failed so need to recall the file selection
+ startExport();
+ }
+
+
+ /**
+ * Export the information to the given writer
+ * @param inWriter writer object
+ * @return number of points written
+ */
+ private int exportData(OutputStreamWriter inWriter) throws IOException
+ {
+ inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<gpx version=\"");
+ inWriter.write(GPX_VERSION_NUMBER);
+ inWriter.write("\" creator=\"");
+ inWriter.write(GPX_CREATOR);
+ inWriter.write("\">\n");
+ // Name field
+ if (_nameField != null && _nameField.getText() != null && !_nameField.getText().equals(""))
+ {
+ inWriter.write("\t<name>");
+ inWriter.write(_nameField.getText());
+ inWriter.write("</name>\n");
+ }
+ // Description field
+ inWriter.write("\t<desc>");
+ if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals(""))
+ {
+ inWriter.write(_descriptionField.getText());
+ }
+ else
+ {
+ inWriter.write("Export from Prune");
+ }
+ inWriter.write("</desc>\n");
+
+ int i = 0;
+ DataPoint point = null;
+ boolean hasTrackpoints = false;
+ // Loop over waypoints
+ int numPoints = _track.getNumPoints();
+ for (i=0; i<numPoints; i++)
+ {
+ point = _track.getPoint(i);
+ // Make a blob for each waypoint
+ if (point.isWaypoint())
+ {
+ exportWaypoint(point, inWriter);
+ }
+ else
+ {
+ hasTrackpoints = true;
+ }
+ }
+ // Make a line for the track, if there is one
+ // TODO: Look at segments of track, and split into separate track segments in Gpx if necessary
+ if (hasTrackpoints)
+ {
+ inWriter.write("\t<trk><trkseg>\n");
+ // Loop over track points
+ for (i=0; i<numPoints; i++)
+ {
+ point = _track.getPoint(i);
+ if (!point.isWaypoint())
+ {
+ exportTrackpoint(point, inWriter);
+ }
+ }
+ inWriter.write("\t</trkseg></trk>\n");
+ }
+ inWriter.write("</gpx>\n");
+ return numPoints;
+ }
+
+
+ /**
+ * Export the specified waypoint into the file
+ * @param inPoint waypoint to export
+ * @param inWriter writer object
+ * @throws IOException on write failure
+ */
+ private void exportWaypoint(DataPoint inPoint, Writer inWriter) throws IOException
+ {
+ inWriter.write("\t<wpt lat=\"");
+ inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
+ inWriter.write("\" lon=\"");
+ inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
+ inWriter.write("\">\n");
+ inWriter.write("\t\t<name>");
+ inWriter.write(inPoint.getWaypointName().trim());
+ inWriter.write("</name>\n");
+ // altitude if available
+ if (inPoint.hasAltitude())
+ {
+ inWriter.write("\t\t<ele>");
+ inWriter.write("" + inPoint.getAltitude().getValue(Altitude.FORMAT_METRES));
+ inWriter.write("</ele>\n");
+ }
+ // timestamp if available (point might have altitude and then be turned into a waypoint)
+ if (inPoint.hasTimestamp())
+ {
+ inWriter.write("\t\t<time>");
+ inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
+ inWriter.write("</time>\n");
+ }
+ // TODO: Include waypt type in Gpx
+ inWriter.write("\t</wpt>\n");
+ }
+
+
+ /**
+ * Export the specified trackpoint into the file
+ * @param inPoint trackpoint to export
+ * @param inWriter writer object
+ */
+ private void exportTrackpoint(DataPoint inPoint, Writer inWriter) throws IOException
+ {
+ inWriter.write("\t\t<trkpt lat=\"");
+ inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
+ inWriter.write("\" lon=\"");
+ inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
+ inWriter.write("\">");
+ // altitude
+ if (inPoint.hasAltitude())
+ {
+ inWriter.write("<ele>");
+ inWriter.write("" + inPoint.getAltitude().getValue(Altitude.FORMAT_METRES));
+ inWriter.write("</ele>");
+ }
+ // timestamp if available
+ if (inPoint.hasTimestamp())
+ {
+ inWriter.write("<time>");
+ inWriter.write(inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
+ inWriter.write("</time>");
+ }
+ inWriter.write("</trkpt>\n");
+ }
+}
import javax.swing.filechooser.FileFilter;
import tim.prune.I18nManager;
+import tim.prune.data.Altitude;
import tim.prune.data.Coordinate;
import tim.prune.data.DataPoint;
+import tim.prune.data.Field;
import tim.prune.data.Track;
import tim.prune.data.TrackInfo;
import tim.prune.gui.ImageUtils;
private Track _track = null;
private JDialog _dialog = null;
private JTextField _descriptionField = null;
+ private JCheckBox _altitudesCheckbox = null;
private JCheckBox _kmzCheckbox = null;
private JCheckBox _exportImagesCheckbox = null;
private JProgressBar _progressBar = null;
descPanel.add(_descriptionField);
mainPanel.add(descPanel);
dialogPanel.add(mainPanel, BorderLayout.CENTER);
+ // Checkbox for altitude export
+ _altitudesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.altitude"));
+ _altitudesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
+ mainPanel.add(_altitudesCheckbox);
// Checkboxes for kmz export and image export
_kmzCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.kmz"));
_kmzCheckbox.setHorizontalTextPosition(SwingConstants.LEFT);
*/
private void enableCheckboxes()
{
+ boolean hasAltitudes = _track.hasData(Field.ALTITUDE);
+ if (!hasAltitudes) {_altitudesCheckbox.setSelected(false);}
boolean hasPhotos = _trackInfo.getPhotoList() != null && _trackInfo.getPhotoList().getNumPhotos() > 0;
_exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected());
_exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected());
}
inWriter.write("</name>\n");
+ boolean exportAltitudes = _altitudesCheckbox.isSelected();
int i = 0;
DataPoint point = null;
boolean hasTrackpoints = false;
// Make a blob for each waypoint
if (point.isWaypoint())
{
- exportWaypoint(point, inWriter);
+ exportWaypoint(point, inWriter, exportAltitudes);
+ }
+ else if (point.getPhoto() == null)
+ {
+ hasTrackpoints = true;
}
// Make a blob with description for each photo
if (point.getPhoto() != null)
writtenPhotoHeader = true;
}
photoNum++;
- exportPhotoPoint(point, inWriter, inExportImages, photoNum);
- }
- else
- {
- hasTrackpoints = true;
+ exportPhotoPoint(point, inWriter, inExportImages, photoNum, exportAltitudes);
}
}
// Make a line for the track, if there is one
{
inWriter.write("\t<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
+ "\t\t\t\t<color>cc0000cc</color>\n\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
- + "\t\t</Style>\n\t\t<LineString>\n\t\t\t<coordinates>");
+ + "\t\t\t<PolyStyle><color>33cc0000</color></PolyStyle>\n"
+ + "\t\t</Style>\n\t\t<LineString>\n");
+ if (exportAltitudes) {
+ inWriter.write("\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n");
+ }
+ inWriter.write("\t\t\t<coordinates>");
// Loop over track points
for (i=0; i<numPoints; i++)
{
point = _track.getPoint(i);
- if (!point.isWaypoint())
+ if (!point.isWaypoint() && point.getPhoto() == null)
{
- exportTrackpoint(point, inWriter);
+ exportTrackpoint(point, inWriter, exportAltitudes);
}
}
inWriter.write("\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>");
* Export the specified waypoint into the file
* @param inPoint waypoint to export
* @param inWriter writer object
+ * @param inExportAltitude true to include altitude
* @throws IOException on write failure
*/
- private void exportWaypoint(DataPoint inPoint, Writer inWriter) throws IOException
+ private void exportWaypoint(DataPoint inPoint, Writer inWriter, boolean inExportAltitude) throws IOException
{
inWriter.write("\t<Placemark>\n\t\t<name>");
inWriter.write(inPoint.getWaypointName().trim());
inWriter.write("</name>\n");
- inWriter.write("\t\t<Point>\n\t\t\t<coordinates>");
+ inWriter.write("\t\t<Point>\n");
+ if (inExportAltitude && inPoint.hasAltitude()) {
+ inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
+ }
+ inWriter.write("\t\t\t<coordinates>");
inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
inWriter.write(',');
inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
- inWriter.write(",0</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
+ inWriter.write(",");
+ if (inExportAltitude && inPoint.hasAltitude()) {
+ inWriter.write("" + inPoint.getAltitude().getValue(Altitude.FORMAT_METRES));
+ }
+ else {
+ inWriter.write("0");
+ }
+ inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
}
* @param inWriter writer object
* @param inImageLink flag to set whether to export image links or not
* @param inImageNumber number of image for filename
+ * @param inExportAltitude true to include altitude
* @throws IOException on write failure
*/
- private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink, int inImageNumber)
+ private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink,
+ int inImageNumber, boolean inExportAltitude)
throws IOException
{
inWriter.write("\t<Placemark>\n\t\t<name>");
+ "<tr><td><center>Caption for the photo</center></td></tr></table>]]></description>");
}
inWriter.write("<styleUrl>#camera_icon</styleUrl>\n");
- inWriter.write("\t\t<Point>\n\t\t\t<coordinates>");
+ inWriter.write("\t\t<Point>\n");
+ if (inExportAltitude && inPoint.hasAltitude()) {
+ inWriter.write("\t\t\t<altitudeMode>absolute</altitudeMode>\n");
+ }
+ inWriter.write("\t\t\t<coordinates>");
inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
inWriter.write(',');
inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
- inWriter.write(",0</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
+ inWriter.write(",");
+ if (inExportAltitude && inPoint.hasAltitude()) {
+ inWriter.write("" + inPoint.getAltitude().getValue(Altitude.FORMAT_METRES));
+ }
+ else {
+ inWriter.write("0");
+ }
+ inWriter.write("</coordinates>\n\t\t</Point>\n\t</Placemark>\n");
}
* Export the specified trackpoint into the file
* @param inPoint trackpoint to export
* @param inWriter writer object
+ * @param inExportAltitude true to include altitude
*/
- private void exportTrackpoint(DataPoint inPoint, Writer inWriter) throws IOException
+ private void exportTrackpoint(DataPoint inPoint, Writer inWriter, boolean inExportAltitude) throws IOException
{
inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
inWriter.write(',');
inWriter.write(inPoint.getLatitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL));
- // Altitude not exported, locked to ground by Google Earth
- inWriter.write(",0\n");
+ // Altitude either absolute or locked to ground by Google Earth
+ inWriter.write(",");
+ if (inExportAltitude && inPoint.hasAltitude()) {
+ inWriter.write("" + inPoint.getAltitude().getValue(Altitude.FORMAT_METRES));
+ }
+ else {
+ inWriter.write("0");
+ }
+ inWriter.write("\n");
}
/**
* Constructor giving list size
+ * @param inSize number of photos
*/
public PhotoTableModel(int inSize)
{
{
return _photos[inRowIndex].getStatus();
}
- return new Boolean(_photos[inRowIndex].getSaveFlag());
+ return Boolean.valueOf(_photos[inRowIndex].getSaveFlag());
}
JPanel flowPanel = new JPanel();
flowPanel.add(centralPanel);
-
+
// show lines button
JButton showLinesButton = new JButton(I18nManager.getText("button.showlines"));
showLinesButton.addActionListener(new ActionListener() {
{
private JButton _upButton = null;
private JButton _downButton = null;
- private int _maxIndex = 0;
+ private int _maxIndex = 2;
/**
- * Constructor giving buttons and size
+ * Constructor giving buttons to enable/disable
* @param inUpButton up button
* @param inDownButton down button
- * @param inListSize size of list
*/
- public UpDownToggler(JButton inUpButton, JButton inDownButton, int inListSize)
+ public UpDownToggler(JButton inUpButton, JButton inDownButton)
{
_upButton = inUpButton;
_downButton = inDownButton;
- _maxIndex = inListSize - 1;
}
+ /**
+ * Set the list size
+ * @param inListSize number of items in list
+ */
+ public void setListSize(int inListSize)
+ {
+ _maxIndex = inListSize - 1;
+ }
/**
* list selection has changed
/**
* Show the window
+ * @throws ThreeDException when 3d classes not found
*/
public void show() throws ThreeDException;
}
{\r
_point.setPhoto(null);\r
photo.setDataPoint(null);\r
+ // inform subscribers\r
+ inTrackInfo.triggerUpdate();\r
}\r
else\r
{\r
--- /dev/null
+package tim.prune.undo;\r
+\r
+import tim.prune.I18nManager;\r
+import tim.prune.data.DataPoint;\r
+import tim.prune.data.Photo;\r
+import tim.prune.data.TrackInfo;\r
+\r
+/**\r
+ * Operation to undo an auto-correlation of photos with points\r
+ */\r
+public class UndoCorrelatePhotos implements UndoOperation\r
+{\r
+ private DataPoint[] _contents = null;\r
+ private DataPoint[] _photoPoints = null;\r
+ private int _numPhotosCorrelated = -1;\r
+\r
+\r
+ /**\r
+ * Constructor\r
+ * @param inTrackInfo track information\r
+ */\r
+ public UndoCorrelatePhotos(TrackInfo inTrackInfo)\r
+ {\r
+ // Copy track contents\r
+ _contents = inTrackInfo.getTrack().cloneContents();\r
+ // Copy points associated with photos before correlation\r
+ int numPhotos = inTrackInfo.getPhotoList().getNumPhotos();\r
+ _photoPoints = new DataPoint[numPhotos];\r
+ for (int i=0; i<numPhotos; i++) {\r
+ _photoPoints[i] = inTrackInfo.getPhotoList().getPhoto(i).getDataPoint();\r
+ }\r
+ }\r
+\r
+ /**\r
+ * @param inNumCorrelated number of photos correlated\r
+ */\r
+ public void setNumPhotosCorrelated(int inNumCorrelated)\r
+ {\r
+ _numPhotosCorrelated = inNumCorrelated;\r
+ }\r
+\r
+ /**\r
+ * @return description of operation including parameters\r
+ */\r
+ public String getDescription()\r
+ {\r
+ return I18nManager.getText("undo.correlate") + " (" + _numPhotosCorrelated + ")";\r
+ }\r
+\r
+\r
+ /**\r
+ * Perform the undo operation on the given Track\r
+ * @param inTrackInfo TrackInfo object on which to perform the operation\r
+ */\r
+ public void performUndo(TrackInfo inTrackInfo) throws UndoException\r
+ {\r
+ // restore track to previous values\r
+ inTrackInfo.getTrack().replaceContents(_contents);\r
+ // restore photo association\r
+ for (int i=0; i<_photoPoints.length; i++)\r
+ {\r
+ Photo photo = inTrackInfo.getPhotoList().getPhoto(i);\r
+ DataPoint point = _photoPoints[i];\r
+ photo.setDataPoint(point);\r
+ if (point != null) {\r
+ point.setPhoto(photo);\r
+ }\r
+ }\r
+ // clear selection\r
+ inTrackInfo.getSelection().clearAll();\r
+ }\r
+}
\ No newline at end of file
\r
/**\r
* Constructor\r
- * @param inIndex index number of point within track\r
- * @param inPoint data point\r
+ * @param inTrackInfo track info object\r
*/\r
public UndoDeleteRange(TrackInfo inTrackInfo)\r
{\r
--- /dev/null
+package tim.prune.undo;\r
+\r
+import tim.prune.I18nManager;\r
+import tim.prune.data.DataPoint;\r
+import tim.prune.data.Photo;\r
+import tim.prune.data.TrackInfo;\r
+\r
+/**\r
+ * Operation to undo the disconnection of a photo from a point\r
+ */\r
+public class UndoDisconnectPhoto implements UndoOperation\r
+{\r
+ private DataPoint _point = null;\r
+ private Photo _photo = null;\r
+ private String _filename = null;\r
+\r
+\r
+ /**\r
+ * Constructor\r
+ * @param inPoint data point\r
+ * @param inFilename filename of photo\r
+ */\r
+ public UndoDisconnectPhoto(DataPoint inPoint, String inFilename)\r
+ {\r
+ _point = inPoint;\r
+ _photo = inPoint.getPhoto();\r
+ _filename = inFilename;\r
+ }\r
+\r
+\r
+ /**\r
+ * @return description of operation including photo filename\r
+ */\r
+ public String getDescription()\r
+ {\r
+ String desc = I18nManager.getText("undo.disconnectphoto") + " " + _filename;\r
+ return desc;\r
+ }\r
+\r
+\r
+ /**\r
+ * Perform the undo operation on the given Track\r
+ * @param inTrackInfo TrackInfo object on which to perform the operation\r
+ */\r
+ public void performUndo(TrackInfo inTrackInfo) throws UndoException\r
+ {\r
+ // Connect again\r
+ if (_point != null && _photo != null)\r
+ {\r
+ _point.setPhoto(_photo);\r
+ _photo.setDataPoint(_point);\r
+ // inform subscribers\r
+ inTrackInfo.triggerUpdate();\r
+ }\r
+ else\r
+ {\r
+ // throw exception if failed\r
+ throw new UndoException(getDescription());\r
+ }\r
+ }\r
+}
\ No newline at end of file
\r
/**\r
* Constructor for replacing\r
- * @param inOldTrack track being replaced\r
+ * @param inOldTrackInfo track info being replaced\r
* @param inNumLoaded number of points loaded\r
* @param inPhotoList photo list, if any\r
*/\r
/**\r
* Perform the undo operation on the specified track\r
* @param inTrackInfo TrackInfo object on which to perform the operation\r
+ * @throws UndoException when undo fails\r
*/\r
public void performUndo(TrackInfo inTrackInfo) throws UndoException;\r
}
\ No newline at end of file