From 5625a1abadb5f2ca5f017fe7dbda1d5141cb637b Mon Sep 17 00:00:00 2001 From: activityworkshop Date: Sat, 14 Feb 2015 14:43:11 +0100 Subject: [PATCH] Version 3, August 2007 --- tim/prune/App.java | 262 ++++++++- tim/prune/ExternalTools.java | 48 ++ tim/prune/GpsPruner.java | 65 ++- tim/prune/UpdateMessageBroker.java | 2 +- tim/prune/data/Altitude.java | 2 +- tim/prune/data/Coordinate.java | 32 +- tim/prune/data/DataPoint.java | 11 +- tim/prune/data/Distance.java | 4 +- tim/prune/data/Photo.java | 133 ++++- tim/prune/data/PhotoList.java | 171 +++++- tim/prune/data/PhotoStatus.java | 11 + tim/prune/data/Selection.java | 43 ++ tim/prune/data/Timestamp.java | 38 +- tim/prune/data/Track.java | 27 +- tim/prune/data/TrackInfo.java | 120 ++++- tim/prune/edit/PointNameEditor.java | 43 +- tim/prune/gui/AboutScreen.java | 167 +++++- tim/prune/gui/DetailsDisplay.java | 348 ++---------- tim/prune/gui/ImageUtils.java | 86 +++ tim/prune/gui/MapChart.java | 29 +- tim/prune/gui/MenuManager.java | 233 ++++++-- tim/prune/gui/PhotoThumbnail.java | 123 +++++ tim/prune/gui/ProfileChart.java | 4 +- tim/prune/gui/SelectorDisplay.java | 259 +++++++++ tim/prune/gui/images/add_photo_icon.png | Bin 0 -> 339 bytes tim/prune/gui/images/add_textfile_icon.png | Bin 0 -> 484 bytes tim/prune/gui/images/connect_photo_icon.png | Bin 0 -> 286 bytes tim/prune/gui/images/edit_point_icon.gif | Bin 0 -> 577 bytes tim/prune/gui/images/save_icon.gif | Bin 0 -> 639 bytes tim/prune/gui/images/set_end_icon.png | Bin 0 -> 627 bytes tim/prune/gui/images/set_start_icon.png | Bin 0 -> 667 bytes tim/prune/gui/images/undo_icon.gif | Bin 0 -> 355 bytes tim/prune/lang/prune-texts.properties | 69 ++- tim/prune/lang/prune-texts_de.properties | 71 ++- tim/prune/lang/prune-texts_de_CH.properties | 127 +++-- tim/prune/lang/prune-texts_es.properties | 73 ++- tim/prune/lang/prune-texts_fr.properties | 309 +++++++++++ tim/prune/load/FileCacher.java | 13 +- tim/prune/load/FileLoader.java | 542 ++----------------- tim/prune/load/JpegLoader.java | 156 ++++-- tim/prune/load/PhotoMeasurer.java | 60 +++ tim/prune/load/TextFileLoader.java | 561 ++++++++++++++++++++ tim/prune/load/xml/GpxHandler.java | 157 ++++++ tim/prune/load/xml/KmlHandler.java | 150 ++++++ tim/prune/load/xml/XmlFileLoader.java | 148 ++++++ tim/prune/load/xml/XmlHandler.java | 22 + tim/prune/readme.txt | 15 +- tim/prune/save/ExifSaver.java | 366 +++++++++++++ tim/prune/save/KmlExporter.java | 519 +++++++++++++----- tim/prune/save/PhotoTableEntry.java | 98 ++++ tim/prune/save/PhotoTableModel.java | 138 +++++ tim/prune/save/PovExporter.java | 32 +- tim/prune/threedee/Java3DWindow.java | 33 +- tim/prune/threedee/LineDialog.java | 111 ++++ tim/prune/threedee/ThreeDModel.java | 12 +- tim/prune/threedee/WindowFactory.java | 4 +- tim/prune/undo/UndoConnectPhoto.java | 58 ++ tim/prune/undo/UndoDeletePhoto.java | 73 +++ tim/prune/undo/UndoDeletePoint.java | 26 +- tim/prune/undo/UndoDeleteRange.java | 14 + tim/prune/undo/UndoEditPoint.java | 3 +- tim/prune/undo/UndoLoad.java | 11 +- tim/prune/undo/UndoLoadPhotos.java | 27 +- 63 files changed, 5039 insertions(+), 1220 deletions(-) create mode 100644 tim/prune/ExternalTools.java create mode 100644 tim/prune/data/PhotoStatus.java create mode 100644 tim/prune/gui/ImageUtils.java create mode 100644 tim/prune/gui/PhotoThumbnail.java create mode 100644 tim/prune/gui/SelectorDisplay.java create mode 100644 tim/prune/gui/images/add_photo_icon.png create mode 100644 tim/prune/gui/images/add_textfile_icon.png create mode 100644 tim/prune/gui/images/connect_photo_icon.png create mode 100644 tim/prune/gui/images/edit_point_icon.gif create mode 100644 tim/prune/gui/images/save_icon.gif create mode 100644 tim/prune/gui/images/set_end_icon.png create mode 100644 tim/prune/gui/images/set_start_icon.png create mode 100644 tim/prune/gui/images/undo_icon.gif create mode 100644 tim/prune/lang/prune-texts_fr.properties create mode 100644 tim/prune/load/PhotoMeasurer.java create mode 100644 tim/prune/load/TextFileLoader.java create mode 100644 tim/prune/load/xml/GpxHandler.java create mode 100644 tim/prune/load/xml/KmlHandler.java create mode 100644 tim/prune/load/xml/XmlFileLoader.java create mode 100644 tim/prune/load/xml/XmlHandler.java create mode 100644 tim/prune/save/ExifSaver.java create mode 100644 tim/prune/save/PhotoTableEntry.java create mode 100644 tim/prune/save/PhotoTableModel.java create mode 100644 tim/prune/threedee/LineDialog.java create mode 100644 tim/prune/undo/UndoConnectPhoto.java create mode 100644 tim/prune/undo/UndoDeletePhoto.java diff --git a/tim/prune/App.java b/tim/prune/App.java index 4ccaed6..e403722 100644 --- a/tim/prune/App.java +++ b/tim/prune/App.java @@ -9,6 +9,8 @@ import javax.swing.JOptionPane; import tim.prune.data.DataPoint; import tim.prune.data.Field; +import tim.prune.data.Photo; +import tim.prune.data.PhotoList; import tim.prune.data.Track; import tim.prune.data.TrackInfo; import tim.prune.edit.FieldEditList; @@ -18,6 +20,8 @@ import tim.prune.gui.MenuManager; 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.KmlExporter; import tim.prune.save.PovExporter; @@ -25,7 +29,9 @@ import tim.prune.threedee.ThreeDException; import tim.prune.threedee.ThreeDWindow; import tim.prune.threedee.WindowFactory; import tim.prune.undo.UndoCompress; +import tim.prune.undo.UndoConnectPhoto; import tim.prune.undo.UndoDeleteDuplicates; +import tim.prune.undo.UndoDeletePhoto; import tim.prune.undo.UndoDeletePoint; import tim.prune.undo.UndoDeleteRange; import tim.prune.undo.UndoEditPoint; @@ -51,6 +57,7 @@ public class App private MenuManager _menuManager = null; private FileLoader _fileLoader = null; private JpegLoader _jpegLoader = null; + private KmlExporter _exporter = null; private PovExporter _povExporter = null; private Stack _undoStack = null; private UpdateMessageBroker _broker = null; @@ -91,7 +98,8 @@ public class App */ public boolean hasDataUnsaved() { - return _undoStack.size() > _lastSavePosition; + return (_undoStack.size() > _lastSavePosition + && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().getNumPhotos() > 0)); } /** @@ -164,8 +172,12 @@ public class App } else { - KmlExporter exporter = new KmlExporter(this, _frame, _track); - exporter.showDialog(); + // Invoke the export + if (_exporter == null) + { + _exporter = new KmlExporter(_frame, _trackInfo); + } + _exporter.showDialog(); } } @@ -210,7 +222,7 @@ public class App // Make new exporter if necessary if (_povExporter == null) { - _povExporter = new PovExporter(this, _frame, _track); + _povExporter = new PovExporter(_frame, _track); } // Specify angles if necessary if (inDefineSettings) @@ -229,6 +241,9 @@ public class App */ public void exit() { + // grab focus + _frame.toFront(); + _frame.requestFocus(); // check if ok to exit Object[] buttonTexts = {I18nManager.getText("button.exit"), I18nManager.getText("button.cancel")}; if (!hasDataUnsaved() @@ -292,7 +307,7 @@ public class App { // Open point dialog to display details PointNameEditor editor = new PointNameEditor(this, _frame); - editor.showDialog(_track, currentPoint); + editor.showDialog(currentPoint); } } } @@ -308,13 +323,44 @@ public class App DataPoint currentPoint = _trackInfo.getCurrentPoint(); if (currentPoint != null) { + boolean deletePhoto = false; + Photo currentPhoto = currentPoint.getPhoto(); + if (currentPhoto != null) + { + // Confirm deletion of photo or decoupling + int response = JOptionPane.showConfirmDialog(_frame, + I18nManager.getText("dialog.deletepoint.deletephoto") + " " + currentPhoto.getFile().getName(), + I18nManager.getText("dialog.deletepoint.title"), + JOptionPane.YES_NO_CANCEL_OPTION); + if (response == JOptionPane.CANCEL_OPTION || response == JOptionPane.CLOSED_OPTION) + { + // cancel pressed- abort delete + return; + } + if (response == JOptionPane.YES_OPTION) {deletePhoto = true;} + } // add information to undo stack int pointIndex = _trackInfo.getSelection().getCurrentPointIndex(); - UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint); + int photoIndex = _trackInfo.getPhotoList().getPhotoIndex(currentPhoto); + // Undo object needs to know index of photo in list (if any) to restore + UndoOperation undo = new UndoDeletePoint(pointIndex, currentPoint, photoIndex); // call track to delete point if (_trackInfo.deletePoint()) { _undoStack.push(undo); + if (currentPhoto != null) + { + // delete photo if necessary + if (deletePhoto) + { + _trackInfo.getPhotoList().deletePhoto(photoIndex); + } + else + { + // decouple photo from point + currentPhoto.setDataPoint(null); + } + } } } } @@ -328,12 +374,77 @@ public class App { if (_track != null) { - // add information to undo stack - UndoOperation undo = new UndoDeleteRange(_trackInfo); - // call track to delete point - if (_trackInfo.deleteRange()) + // Find out if photos should be deleted or not + int selStart = _trackInfo.getSelection().getStart(); + int selEnd = _trackInfo.getSelection().getEnd(); + if (selStart >= 0 && selEnd >= selStart) { - _undoStack.push(undo); + int numToDelete = selEnd - selStart + 1; + boolean[] deletePhotos = new boolean[numToDelete]; + Photo[] photosToDelete = new Photo[numToDelete]; + boolean deleteAll = false; + boolean deleteNone = false; + String[] questionOptions = {I18nManager.getText("button.yes"), I18nManager.getText("button.no"), + I18nManager.getText("button.yestoall"), I18nManager.getText("button.notoall"), + I18nManager.getText("button.cancel")}; + DataPoint point = null; + for (int i=0; i 0) { @@ -548,25 +672,41 @@ public class App if (answer == JOptionPane.YES_OPTION) { // append data to current Track - Track loadedTrack = new Track(_broker); - loadedTrack.load(inFieldArray, inDataArray, inAltFormat); _undoStack.add(new UndoLoad(_track.getNumPoints(), loadedTrack.getNumPoints())); _track.combine(loadedTrack); - _trackInfo.getFileInfo().addFile(); + // set filename if currently empty + if (_trackInfo.getFileInfo().getNumFiles() == 0) + { + _trackInfo.getFileInfo().setFile(inFilename); + } + else + { + _trackInfo.getFileInfo().addFile(); + } } else if (answer == JOptionPane.NO_OPTION) { // Don't append, replace data - _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length)); + PhotoList photos = null; + if (_trackInfo.getPhotoList().hasCorrelatedPhotos()) + { + photos = _trackInfo.getPhotoList().cloneList(); + } + _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, photos)); _lastSavePosition = _undoStack.size(); + // TODO: Should be possible to reuse the Track object already loaded? _trackInfo.loadTrack(inFieldArray, inDataArray, inAltFormat); _trackInfo.getFileInfo().setFile(inFilename); + if (photos != null) + { + _trackInfo.getPhotoList().removeCorrelatedPhotos(); + } } } else { // currently no data held, so use received data - _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length)); + _undoStack.add(new UndoLoad(_trackInfo, inDataArray.length, null)); _lastSavePosition = _undoStack.size(); _trackInfo.loadTrack(inFieldArray, inDataArray, inAltFormat); _trackInfo.getFileInfo().setFile(inFilename); @@ -585,22 +725,26 @@ public class App { if (inPhotoList != null && !inPhotoList.isEmpty()) { - // TODO: Attempt to restrict loaded photos to current area (if any) ? - int numAdded = _trackInfo.addPhotos(inPhotoList); - if (numAdded > 0) + int[] numsAdded = _trackInfo.addPhotos(inPhotoList); + int numPhotosAdded = numsAdded[0]; + int numPointsAdded = numsAdded[1]; + if (numPhotosAdded > 0) { - _undoStack.add(new UndoLoadPhotos(numAdded)); + // 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 (numAdded == 1) + if (numPhotosAdded == 1) { JOptionPane.showMessageDialog(_frame, - "" + numAdded + " " + I18nManager.getText("dialog.jpegload.photoadded"), + "" + numPhotosAdded + " " + I18nManager.getText("dialog.jpegload.photoadded"), I18nManager.getText("dialog.jpegload.title"), JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(_frame, - "" + numAdded + " " + I18nManager.getText("dialog.jpegload.photosadded"), + "" + numPhotosAdded + " " + I18nManager.getText("dialog.jpegload.photosadded"), I18nManager.getText("dialog.jpegload.title"), JOptionPane.INFORMATION_MESSAGE); } // TODO: Improve message when photo(s) fail to load (eg already added) @@ -611,6 +755,78 @@ public class App } + /** + * Connect the current photo to the current point + */ + public void connectPhotoToPoint() + { + Photo photo = _trackInfo.getCurrentPhoto(); + DataPoint point = _trackInfo.getCurrentPoint(); + if (photo != null && point != null && point.getPhoto() == null) + { + // connect + _undoStack.add(new UndoConnectPhoto(point, photo.getFile().getName())); + photo.setDataPoint(point); + point.setPhoto(photo); + //TODO: Confirm connect (maybe with status in photo panel?) + } + } + + + /** + * Remove the current photo, if any + */ + public void deleteCurrentPhoto() + { + // Delete the current photo, and optionally its point too, keeping undo information + Photo currentPhoto = _trackInfo.getCurrentPhoto(); + if (currentPhoto != null) + { + // Photo is selected, see if it has a point or not + boolean photoDeleted = false; + UndoDeletePhoto undoAction = null; + if (currentPhoto.getDataPoint() == null) + { + // no point attached, so just delete photo + undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(), + null, -1); + photoDeleted = _trackInfo.deleteCurrentPhoto(false); + } + else + { + // point is attached, so need to confirm point deletion + undoAction = new UndoDeletePhoto(currentPhoto, _trackInfo.getSelection().getCurrentPhotoIndex(), + currentPhoto.getDataPoint(), _trackInfo.getTrack().getPointIndex(currentPhoto.getDataPoint())); + int response = JOptionPane.showConfirmDialog(_frame, + I18nManager.getText("dialog.deletephoto.deletepoint"), + I18nManager.getText("dialog.deletephoto.title"), + JOptionPane.YES_NO_CANCEL_OPTION); + boolean deletePointToo = (response == JOptionPane.YES_OPTION); + // Cancel delete if cancel pressed or dialog closed + if (response == JOptionPane.YES_OPTION || response == JOptionPane.NO_OPTION) + { + photoDeleted = _trackInfo.deleteCurrentPhoto(deletePointToo); + } + } + // Add undo information to stack if necessary + if (photoDeleted) + { + _undoStack.add(undoAction); + } + } + } + + + /** + * Save the coordinates of photos in their exif data + */ + public void saveExif() + { + ExifSaver saver = new ExifSaver(_frame); + saver.saveExifInformation(_trackInfo.getPhotoList()); + } + + /** * Inform the app that the data has been saved */ diff --git a/tim/prune/ExternalTools.java b/tim/prune/ExternalTools.java new file mode 100644 index 0000000..cd65fff --- /dev/null +++ b/tim/prune/ExternalTools.java @@ -0,0 +1,48 @@ +package tim.prune; + +import java.io.IOException; + + +/** + * Class to manage interfaces to external tools, like exiftool + */ +public abstract class ExternalTools +{ + + /** + * Attempt to call Povray to see if it's installed / available in path + * @return true if found, false otherwise + */ + public static boolean isPovrayInstalled() + { + try + { + Runtime.getRuntime().exec("povray"); + return true; + } + catch (IOException ioe) + { + // exception thrown, povray not found + return false; + } + } + + + /** + * Attempt to call Exiftool to see if it's installed / available in path + * @return true if found, false otherwise + */ + public static boolean isExiftoolInstalled() + { + try + { + Runtime.getRuntime().exec("exiftool -v"); + return true; + } + catch (IOException ioe) + { + // exception thrown, exiftool not found + return false; + } + } +} diff --git a/tim/prune/GpsPruner.java b/tim/prune/GpsPruner.java index 7e9476c..2511cf7 100644 --- a/tim/prune/GpsPruner.java +++ b/tim/prune/GpsPruner.java @@ -1,26 +1,29 @@ package tim.prune; +import java.awt.BorderLayout; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.Locale; import javax.swing.JFrame; import javax.swing.JSplitPane; +import javax.swing.JToolBar; import javax.swing.WindowConstants; import tim.prune.gui.DetailsDisplay; import tim.prune.gui.MapChart; import tim.prune.gui.MenuManager; import tim.prune.gui.ProfileChart; +import tim.prune.gui.SelectorDisplay; /** * Tool to visualize, edit and prune GPS data */ public class GpsPruner { - // Version 2, released 29 March 2007, 1 April 2007 - public static final String VERSION_NUMBER = "2"; - public static final String BUILD_NUMBER = "056"; + // Final release of version 3 + public static final String VERSION_NUMBER = "3"; + public static final String BUILD_NUMBER = "074"; private static App APP = null; @@ -35,23 +38,44 @@ public class GpsPruner { if (args[0].startsWith("--locale=")) { - if (args[0].length() == 11) - locale = new Locale(args[0].substring(9)); - else if (args[0].length() == 14) - locale = new Locale(args[0].substring(9, 11), args[0].substring(12)); - else - System.out.println("Unrecognised locale '" + args[0].substring(9) - + "' - locale should be eg 'DE' or 'DE_ch'"); + locale = getLanguage(args[0].substring(9)); + } + else if (args[0].startsWith("--lang=")) + { + locale = getLanguage(args[0].substring(7)); } else + { System.out.println("Unknown parameter '" + args[0] + - "'. Possible parameters:\n --locale= used for overriding locale settings\n"); + "'. Possible parameters:\n --locale= or --lang= used for overriding language settings\n"); + } } I18nManager.init(locale); launch(); } + /** + * Choose a locale based on the given code + * @param inString code for locale + * @return Locale object if available, otherwise null + */ + private static Locale getLanguage(String inString) + { + if (inString.length() == 2) + { + return new Locale(inString); + } + else if (inString.length() == 5) + { + return new Locale(inString.substring(0, 2), inString.substring(3)); + } + System.out.println("Unrecognised locale '" + inString + + "' - value should be eg 'DE' or 'DE_ch'"); + return null; + } + + /** * Launch the main application */ @@ -66,19 +90,28 @@ public class GpsPruner frame.setJMenuBar(menuManager.createMenuBar()); APP.setMenuManager(menuManager); broker.addSubscriber(menuManager); + // Make toolbar for buttons + JToolBar toolbar = menuManager.createToolBar(); // Make three GUI components and add as listeners - DetailsDisplay leftPanel = new DetailsDisplay(APP, APP.getTrackInfo()); + SelectorDisplay leftPanel = new SelectorDisplay(APP.getTrackInfo()); broker.addSubscriber(leftPanel); + DetailsDisplay rightPanel = new DetailsDisplay(APP.getTrackInfo()); + broker.addSubscriber(rightPanel); MapChart mapDisp = new MapChart(APP, APP.getTrackInfo()); broker.addSubscriber(mapDisp); ProfileChart profileDisp = new ProfileChart(APP.getTrackInfo()); broker.addSubscriber(profileDisp); - JSplitPane rightPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, mapDisp, profileDisp); - rightPane.setResizeWeight(1.0); // allocate as much space as poss to map + JSplitPane midPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, mapDisp, profileDisp); + midPane.setResizeWeight(1.0); // allocate as much space as poss to map + JSplitPane triplePane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, midPane, rightPanel); + triplePane.setResizeWeight(1.0); // allocate as much space as poss to map + + frame.getContentPane().setLayout(new BorderLayout()); + frame.getContentPane().add(toolbar, BorderLayout.NORTH); frame.getContentPane().add(new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, - rightPane)); + triplePane), BorderLayout.CENTER); // add closing listener frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { @@ -90,7 +123,7 @@ public class GpsPruner // finish off and display frame frame.pack(); - frame.setSize(600, 450); + frame.setSize(650, 450); frame.show(); } } diff --git a/tim/prune/UpdateMessageBroker.java b/tim/prune/UpdateMessageBroker.java index 417d055..cac2a79 100644 --- a/tim/prune/UpdateMessageBroker.java +++ b/tim/prune/UpdateMessageBroker.java @@ -8,7 +8,7 @@ public class UpdateMessageBroker { private DataSubscriber[] _subscribers; private int _subscriberNum = 0; - private static final int MAXIMUM_NUMBER_SUBSCRIBERS = 4; + private static final int MAXIMUM_NUMBER_SUBSCRIBERS = 5; /** diff --git a/tim/prune/data/Altitude.java b/tim/prune/data/Altitude.java index b55a429..54a3f68 100644 --- a/tim/prune/data/Altitude.java +++ b/tim/prune/data/Altitude.java @@ -25,7 +25,7 @@ public class Altitude { try { - _value = Integer.parseInt(inString.trim()); + _value = (int) Double.parseDouble(inString.trim()); _format = inFormat; _valid = true; } diff --git a/tim/prune/data/Coordinate.java b/tim/prune/data/Coordinate.java index 6d8de61..8a81850 100644 --- a/tim/prune/data/Coordinate.java +++ b/tim/prune/data/Coordinate.java @@ -15,6 +15,9 @@ public abstract class Coordinate public static final int FORMAT_DEG_MIN = 11; public static final int FORMAT_DEG = 12; public static final int FORMAT_DEG_WITHOUT_CARDINAL = 13; + public static final int FORMAT_DEG_WHOLE_MIN = 14; + public static final int FORMAT_DEG_MIN_SEC_WITH_SPACES = 15; + public static final int FORMAT_CARDINAL = 16; public static final int FORMAT_NONE = 19; // Instance variables @@ -134,8 +137,8 @@ public abstract class Coordinate { _asDouble = inValue; // Calculate degrees, minutes, seconds - _degrees = (int) inValue; - double numMins = (Math.abs(_asDouble)-Math.abs(_degrees)) * 60.0; + _degrees = (int) Math.abs(inValue); + double numMins = (Math.abs(_asDouble)-_degrees) * 60.0; _minutes = (int) numMins; double numSecs = (numMins - _minutes) * 60.0; _seconds = (int) numSecs; @@ -204,18 +207,37 @@ public abstract class Coordinate .append(twoDigitString(_minutes)).append('\'') .append(twoDigitString(_seconds)).append('.') .append(_fracs); - answer = buffer.toString(); break; + answer = buffer.toString(); + break; } case FORMAT_DEG_MIN: { answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°" - + (_minutes + _seconds / 60.0 + _fracs / 600.0); break; + + (_minutes + _seconds / 60.0 + _fracs / 600.0) + "'"; + break; + } + case FORMAT_DEG_WHOLE_MIN: + { + answer = "" + PRINTABLE_CARDINALS[_cardinal] + threeDigitString(_degrees) + "°" + + (int) Math.floor(_minutes + _seconds / 60.0 + _fracs / 600.0 + 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); break; + + (_degrees + _minutes / 60.0 + _seconds / 3600.0 + _fracs / 36000.0); + break; + } + case FORMAT_DEG_MIN_SEC_WITH_SPACES: + { + answer = "" + _degrees + " " + _minutes + " " + _seconds + "." + _fracs; + break; + } + case FORMAT_CARDINAL: + { + answer = "" + PRINTABLE_CARDINALS[_cardinal]; + break; } } } diff --git a/tim/prune/data/DataPoint.java b/tim/prune/data/DataPoint.java index cd285d6..46c4acd 100644 --- a/tim/prune/data/DataPoint.java +++ b/tim/prune/data/DataPoint.java @@ -17,7 +17,7 @@ public class DataPoint private Timestamp _timestamp = null; private Photo _photo = null; private String _waypointName = null; - private boolean _pointValid = false; + // private boolean _startOfSegment = false; /** @@ -48,6 +48,7 @@ public class DataPoint _altitude = new Altitude(getFieldValue(Field.ALTITUDE), inAltFormat); _timestamp = new Timestamp(getFieldValue(Field.TIMESTAMP)); _waypointName = getFieldValue(Field.WAYPT_NAME); + // TODO: Parse segment start field (format?) } @@ -60,11 +61,15 @@ public class DataPoint public DataPoint(Coordinate inLatitude, Coordinate inLongitude, Altitude inAltitude) { // Only these three fields are available - _fieldValues = new String[0]; - _fieldList = new FieldList(); + _fieldValues = new String[3]; + Field[] fields = {Field.LATITUDE, Field.LONGITUDE, Field.ALTITUDE}; + _fieldList = new FieldList(fields); _latitude = inLatitude; + _fieldValues[0] = inLatitude.output(Coordinate.FORMAT_DEG_MIN_SEC); _longitude = inLongitude; + _fieldValues[1] = inLongitude.output(Coordinate.FORMAT_DEG_MIN_SEC); _altitude = inAltitude; + if (inAltitude != null) {_fieldValues[2] = "" + inAltitude.getValue(Altitude.FORMAT_METRES);} _timestamp = new Timestamp(null); } diff --git a/tim/prune/data/Distance.java b/tim/prune/data/Distance.java index 77ab6d4..5425873 100644 --- a/tim/prune/data/Distance.java +++ b/tim/prune/data/Distance.java @@ -13,8 +13,8 @@ public abstract class Distance 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 = 1.609344; + //private static final double CONVERT_MILES_TO_KM = 0.621371192; /** diff --git a/tim/prune/data/Photo.java b/tim/prune/data/Photo.java index 52743f7..3646cb5 100644 --- a/tim/prune/data/Photo.java +++ b/tim/prune/data/Photo.java @@ -1,7 +1,10 @@ package tim.prune.data; +import java.awt.Dimension; import java.io.File; +import javax.swing.ImageIcon; + /** * Class to represent a photo and link to DataPoint */ @@ -9,8 +12,18 @@ public class Photo { /** File where photo is stored */ private File _file = null; + /** Timestamp, if any */ + private Timestamp _timestamp = null; /** Associated DataPoint if correlated */ private DataPoint _dataPoint = null; + /** Size of original image */ + private Dimension _size = null; + /** Status of photo when loaded */ + private byte _originalStatus = PhotoStatus.NOT_CONNECTED; + /** Current photo status */ + private byte _currentStatus = PhotoStatus.NOT_CONNECTED; + // TODO: Need to store caption for image? + // TODO: Need to store thumbnail for image? /** @@ -20,7 +33,6 @@ public class Photo public Photo(File inFile) { _file = inFile; - // TODO: Cache photo file contents to allow thumbnail preview } @@ -40,6 +52,15 @@ public class Photo public void setDataPoint(DataPoint inPoint) { _dataPoint = inPoint; + // set status according to point + if (inPoint == null) + { + setCurrentStatus(PhotoStatus.NOT_CONNECTED); + } + else + { + setCurrentStatus(PhotoStatus.CONNECTED); + } } /** @@ -50,6 +71,116 @@ public class Photo return _dataPoint; } + /** + * @param inTimestamp Timestamp of photo + */ + public void setTimestamp(Timestamp inTimestamp) + { + _timestamp = inTimestamp; + } + + /** + * @return timestamp of photo + */ + public Timestamp getTimestamp() + { + return _timestamp; + } + + /** + * Calculate the size of the image (slow) + */ + private void calculateSize() + { + ImageIcon icon = new ImageIcon(_file.getAbsolutePath()); + int width = icon.getIconWidth(); + int height = icon.getIconHeight(); + if (width > 0 && height > 0) + { + _size = new Dimension(width, height); + } + } + + /** + * @return size of image as Dimension object + */ + public Dimension getSize() + { + if (_size == null) + { + calculateSize(); + } + return _size; + } + + /** + * @return width of the image, if known + */ + public int getWidth() + { + if (_size == null) + { + calculateSize(); + if (_size == null) {return -1;} + } + return _size.width; + } + + /** + * @return height of the image, if known + */ + public int getHeight() + { + if (_size == null) + { + calculateSize(); + if (_size == null) {return -1;} + } + return _size.height; + } + + /** + * @param inStatus status of photo when loaded + */ + public void setOriginalStatus(byte inStatus) + { + _originalStatus = inStatus; + _currentStatus = inStatus; + } + + /** + * @return status of photo when it was loaded + */ + public byte getOriginalStatus() + { + return _originalStatus; + } + + /** + * @return current status of photo + */ + public byte getCurrentStatus() + { + return _currentStatus; + } + /** + * @param inStatus current status of photo + */ + public void setCurrentStatus(byte inStatus) + { + _currentStatus = inStatus; + } + + + /** + * Delete the cached data when the Photo is no longer needed + */ + public void resetCachedData() + { + _size = null; + // remove thumbnail too + } + /** * Check if a Photo object refers to the same File as another * @param inOther other Photo object diff --git a/tim/prune/data/PhotoList.java b/tim/prune/data/PhotoList.java index ac10edf..b7c044d 100644 --- a/tim/prune/data/PhotoList.java +++ b/tim/prune/data/PhotoList.java @@ -9,6 +9,24 @@ public class PhotoList { private ArrayList _photos = null; + /** + * Empty constructor + */ + public PhotoList() + { + this(null); + } + + /** + * Constructor + * @param inList ArrayList containing Photo objects + */ + private PhotoList(ArrayList inList) + { + _photos = inList; + } + + /** * @return the number of photos in the list */ @@ -20,20 +38,54 @@ public class PhotoList /** - * Add a List of Photos - * @param inList List containing Photo objects + * Add a Photo to the list + * @param inPhoto Photo object to add */ public void addPhoto(Photo inPhoto) { - // Make sure array is initialised - if (_photos == null) + if (inPhoto != null) { - _photos = new ArrayList(); + // Make sure array is initialised + if (_photos == null) + { + _photos = new ArrayList(); + } + // Add the photo + _photos.add(inPhoto); } - // Add the photo + } + + + /** + * Add a Photo to the list + * @param inPhoto Photo object to add + * @param inIndex index at which to add photo + */ + public void addPhoto(Photo inPhoto, int inIndex) + { if (inPhoto != null) { - _photos.add(inPhoto); + // Make sure array is initialised + if (_photos == null) + { + _photos = new ArrayList(); + } + // Add the photo + _photos.add(inIndex, inPhoto); + } + } + + + /** + * Remove the selected photo from the list + * @param inIndex index number to remove + */ + public void deletePhoto(int inIndex) + { + // Maybe throw exception if this fails? + if (_photos != null) + { + _photos.remove(inIndex); } } @@ -44,20 +96,34 @@ public class PhotoList * @return true if it's already in the list */ public boolean contains(Photo inPhoto) + { + return (getPhotoIndex(inPhoto) > -1); + } + + + /** + * Get the index of the given Photo + * @param inPhoto Photo object to check + * @return index of this Photo in the list, or -1 if not found + */ + public int getPhotoIndex(Photo inPhoto) { // Check if we need to check - if (getNumPhotos() <= 0 || inPhoto == null || inPhoto.getFile() == null) - return false; + int numPhotos = getNumPhotos(); + if (numPhotos <= 0 || inPhoto == null || inPhoto.getFile() == null) + return -1; // Loop around photos in list - for (int i=0; i 0) + { + // Construct new list to copy into + ArrayList listCopy = new ArrayList(); + // Loop over photos in list + for (int i=0; i -1) + { + // select associated point, if any + selectPoint(inPointIndex); + } + else + { + // Check if not already done + check(); + } + } + + + /** + * @return currently selected photo index + */ + public int getCurrentPhotoIndex() + { + // System.out.println("Current photo index = " + _currentPhotoIndex); + return _currentPhotoIndex; + } + + /** * Check that the selection still makes sense * and fire update message to listeners diff --git a/tim/prune/data/Timestamp.java b/tim/prune/data/Timestamp.java index 965853a..ddb3808 100644 --- a/tim/prune/data/Timestamp.java +++ b/tim/prune/data/Timestamp.java @@ -53,6 +53,7 @@ public class Timestamp */ public Timestamp(String inString) { + // TODO: Does it really help to store timestamps in seconds rather than ms? if (inString != null && !inString.equals("")) { // Try to parse into a long @@ -85,7 +86,7 @@ public class Timestamp // Lastly, check garmin offset if (diff4 < smallestDiff) { - // milliseconds since garmin offset + // seconds since garmin offset _seconds = rawValue + GARTRIP_OFFSET; } _valid = true; @@ -111,6 +112,41 @@ public class Timestamp } + /** + * Constructor giving each field value individually + * @param inYear year + * @param inMonth month, beginning with 1 + * @param inDay day of month, beginning with 1 + * @param inHour hour of day, 0-24 + * @param inMinute minute + * @param inSecond seconds + */ + public Timestamp(int inYear, int inMonth, int inDay, int inHour, int inMinute, int inSecond) + { + Calendar cal = Calendar.getInstance(); + cal.set(Calendar.YEAR, inYear); + cal.set(Calendar.MONTH, inMonth - 1); + cal.set(Calendar.DAY_OF_MONTH, inDay); + cal.set(Calendar.HOUR_OF_DAY, inHour); + cal.set(Calendar.MINUTE, inMinute); + cal.set(Calendar.SECOND, inSecond); + cal.set(Calendar.MILLISECOND, 0); + _seconds = cal.getTimeInMillis() / 1000; + _valid = true; + } + + + /** + * Constructor giving millis since 1970 + * @param inMillis + */ + public Timestamp(long inMillis) + { + _seconds = inMillis / 1000; + _valid = true; + } + + /** * @return true if timestamp is valid */ diff --git a/tim/prune/data/Track.java b/tim/prune/data/Track.java index e73fa73..c5a0f16 100644 --- a/tim/prune/data/Track.java +++ b/tim/prune/data/Track.java @@ -57,6 +57,11 @@ public class Track */ public void load(Field[] inFieldArray, Object[][] inPointArray, int inAltFormat) { + if (inFieldArray == null || inPointArray == null) + { + _numPoints = 0; + return; + } // copy field list _masterFieldList = new FieldList(inFieldArray); // make DataPoint object from each point in inPointList @@ -148,7 +153,8 @@ public class Track for (int i=0; i<_numPoints; i++) { boolean keepPoint = true; - if (!_dataPoints[i].isWaypoint()) + // Don't delete waypoints or photo points + if (!_dataPoints[i].isWaypoint() && _dataPoints[i].getPhoto() == null) { // go through newPointArray to check for range for (int j=0; j 0) { + // remember if coordinates have changed + boolean coordsChanged = false; // go through edits one by one int numEdits = inEditList.getNumEdits(); for (int i=0; i 0) { - DataPoint[] dataPoints = new DataPoint[numPhotosToAdd]; + DataPoint[] dataPoints = new DataPoint[numPointsToAdd]; int pointNum = 0; + boolean hasAltitude = false; // Add each Photo in turn for (int i=0; i 0) + { + // add points to track + _track.appendPoints(dataPoints); + // modify track field list + _track.getFieldList().extendList(Field.LATITUDE); + _track.getFieldList().extendList(Field.LONGITUDE); + if (hasAltitude) {_track.getFieldList().extendList(Field.ALTITUDE);} + } } - return numPhotosToAdd; + int[] result = {numPhotosToAdd, numPointsToAdd}; + return result; } @@ -147,8 +178,6 @@ public class TrackInfo */ public boolean deleteRange() { - // TODO: Check whether to delete photos associated with this range - int currPoint = _selection.getCurrentPointIndex(); int startSel = _selection.getStart(); int endSel = _selection.getEnd(); boolean answer = _track.deleteRange(startSel, endSel); @@ -166,7 +195,6 @@ public class TrackInfo { if (_track.deletePoint(_selection.getCurrentPointIndex())) { - // TODO: Check whether to delete photo associated with this point _selection.modifyPointDeleted(); _broker.informSubscribers(); return true; @@ -175,6 +203,43 @@ public class TrackInfo } + /** + * Delete the currently selected photo and optionally its point too + * @param inPointToo true to also delete associated point + * @return true if delete successful + */ + public boolean deleteCurrentPhoto(boolean inPointToo) + { + // delete currently selected photo + int photoIndex = _selection.getCurrentPhotoIndex(); + if (photoIndex >= 0) + { + Photo photo = _photoList.getPhoto(photoIndex); + _photoList.deletePhoto(photoIndex); + // has it got a point? + if (photo.getDataPoint() != null) + { + if (inPointToo) + { + // delete point + int pointIndex = _track.getPointIndex(photo.getDataPoint()); + _track.deletePoint(pointIndex); + } + else + { + // disconnect point from photo + photo.getDataPoint().setPhoto(null); + photo.setDataPoint(null); + } + } + // update subscribers + _selection.modifyPointDeleted(); + _broker.informSubscribers(); + } + return true; + } + + /** * Compress the track to the given resolution * @param inResolution resolution @@ -238,4 +303,35 @@ public class TrackInfo // give to selection _selection.selectPoint(index); } + + /** + * Select the given Photo and its point if any + * @param inPhotoIndex index of photo to select + */ + public void selectPhoto(int inPhotoIndex) + { + // Find Photo object + Photo photo = _photoList.getPhoto(inPhotoIndex); + if (photo != null) + { + // Find point object and its index + int pointIndex = _track.getPointIndex(photo.getDataPoint()); + // give to selection object + _selection.selectPhotoAndPoint(inPhotoIndex, pointIndex); + } + else + { + // no photo, just reset selection + _selection.selectPhotoAndPoint(-1, -1); + } + } + + + /** + * Fire a trigger to all data subscribers + */ + public void triggerUpdate() + { + _broker.informSubscribers(); + } } diff --git a/tim/prune/edit/PointNameEditor.java b/tim/prune/edit/PointNameEditor.java index bf66aaf..3229bc2 100644 --- a/tim/prune/edit/PointNameEditor.java +++ b/tim/prune/edit/PointNameEditor.java @@ -20,7 +20,6 @@ import tim.prune.App; import tim.prune.I18nManager; import tim.prune.data.DataPoint; import tim.prune.data.Field; -import tim.prune.data.Track; /** * Class to manage the display and editing of waypoint names @@ -30,7 +29,6 @@ public class PointNameEditor private App _app = null; private JFrame _parentFrame = null; private JDialog _dialog = null; - private Track _track = null; private DataPoint _point = null; private JTextField _nameField = null; private JButton _okButton = null; @@ -50,12 +48,10 @@ public class PointNameEditor /** * Show the edit point name dialog - * @param inTrack track object * @param inPoint point to edit */ - public void showDialog(Track inTrack, DataPoint inPoint) + public void showDialog(DataPoint inPoint) { - _track = inTrack; _point = inPoint; _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.pointnameedit.title"), true); _dialog.setLocationRelativeTo(_parentFrame); @@ -79,15 +75,34 @@ public class PointNameEditor panel.setLayout(new BorderLayout()); // Create GUI layout for point name editor JPanel centrePanel = new JPanel(); - // centrePanel.set centrePanel.add(new JLabel(I18nManager.getText("dialog.pointnameedit.name") + ":")); + // Make listener to react to ok being pressed + ActionListener okActionListener = new ActionListener() { + public void actionPerformed(ActionEvent e) + { + // Check for empty name + if (_nameField.getText().length() > 0) + { + // update App with edit + confirmEdit(); + _dialog.dispose(); + } + } + }; _nameField = new JTextField(inName, 12); _nameField.addKeyListener(new KeyAdapter() { - public void keyTyped(KeyEvent e) + public void keyReleased(KeyEvent e) { - _okButton.setEnabled(true); + // close dialog if escape pressed + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) + { + _dialog.dispose(); + } + // Enable ok button if name not empty + _okButton.setEnabled(_nameField.getText().length() > 0); } }); + _nameField.addActionListener(okActionListener); centrePanel.add(_nameField); panel.add(centrePanel); JPanel rightPanel = new JPanel(); @@ -98,6 +113,7 @@ public class PointNameEditor { _nameField.setText(_nameField.getText().toUpperCase()); _okButton.setEnabled(true); + _nameField.requestFocus(); } }); rightPanel.add(upperButton); @@ -107,6 +123,7 @@ public class PointNameEditor { _nameField.setText(_nameField.getText().toLowerCase()); _okButton.setEnabled(true); + _nameField.requestFocus(); } }); rightPanel.add(lowerButton); @@ -116,6 +133,7 @@ public class PointNameEditor { _nameField.setText(sentenceCase(_nameField.getText())); _okButton.setEnabled(true); + _nameField.requestFocus(); } }); rightPanel.add(sentenceButton); @@ -133,14 +151,7 @@ public class PointNameEditor lowerPanel.add(cancelButton); _okButton = new JButton(I18nManager.getText("button.ok")); _okButton.setEnabled(false); - _okButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) - { - // update App with edit - confirmEdit(); - _dialog.dispose(); - } - }); + _okButton.addActionListener(okActionListener); lowerPanel.add(_okButton); panel.add(lowerPanel, BorderLayout.SOUTH); return panel; diff --git a/tim/prune/gui/AboutScreen.java b/tim/prune/gui/AboutScreen.java index 09ce454..1e4f4f5 100644 --- a/tim/prune/gui/AboutScreen.java +++ b/tim/prune/gui/AboutScreen.java @@ -1,9 +1,15 @@ package tim.prune.gui; +import java.awt.BorderLayout; import java.awt.Component; +import java.awt.FlowLayout; import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; import javax.swing.BorderFactory; import javax.swing.BoxLayout; @@ -13,15 +19,19 @@ import javax.swing.JEditorPane; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JTabbedPane; +import tim.prune.ExternalTools; import tim.prune.GpsPruner; import tim.prune.I18nManager; +import tim.prune.threedee.WindowFactory; /** * Class to represent the "About" popup window */ public class AboutScreen extends JDialog { + JButton _okButton = null; /** * Constructor @@ -39,19 +49,25 @@ public class AboutScreen extends JDialog private Component makeContents() { JPanel mainPanel = new JPanel(); - mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); - mainPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + mainPanel.setLayout(new BorderLayout()); + + JTabbedPane tabPane = new JTabbedPane(); + mainPanel.add(tabPane, BorderLayout.CENTER); + + JPanel aboutPanel = new JPanel(); + aboutPanel.setLayout(new BoxLayout(aboutPanel, BoxLayout.Y_AXIS)); + aboutPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); JLabel titleLabel = new JLabel("Prune"); titleLabel.setFont(new Font("SansSerif", Font.BOLD, 24)); titleLabel.setAlignmentX(JLabel.CENTER_ALIGNMENT); - mainPanel.add(titleLabel); + aboutPanel.add(titleLabel); JLabel versionLabel = new JLabel(I18nManager.getText("dialog.about.version") + ": " + GpsPruner.VERSION_NUMBER); versionLabel.setAlignmentX(JLabel.CENTER_ALIGNMENT); - mainPanel.add(versionLabel); + aboutPanel.add(versionLabel); JLabel buildLabel = new JLabel(I18nManager.getText("dialog.about.build") + ": " + GpsPruner.BUILD_NUMBER); buildLabel.setAlignmentX(JLabel.CENTER_ALIGNMENT); - mainPanel.add(buildLabel); - mainPanel.add(new JLabel(" ")); + aboutPanel.add(buildLabel); + aboutPanel.add(new JLabel(" ")); StringBuffer descBuffer = new StringBuffer(); descBuffer.append("

").append(I18nManager.getText("dialog.about.summarytext1")).append("

"); descBuffer.append("

").append(I18nManager.getText("dialog.about.summarytext2")).append("

"); @@ -63,21 +79,147 @@ public class AboutScreen extends JDialog descPane.setOpaque(false); descPane.setAlignmentX(JEditorPane.CENTER_ALIGNMENT); - mainPanel.add(descPane); - mainPanel.add(new JLabel(" ")); - JButton okButton = new JButton(I18nManager.getText("button.ok")); - okButton.addActionListener(new ActionListener() + aboutPanel.add(descPane); + aboutPanel.add(new JLabel(" ")); + tabPane.add(I18nManager.getText("dialog.about.title"), aboutPanel); + + // Second pane for system info + JPanel sysInfoPanel = new JPanel(); + GridBagLayout gridBag = new GridBagLayout(); + sysInfoPanel.setLayout(gridBag); + GridBagConstraints constraints = new GridBagConstraints(); + constraints.weightx = 0.0; constraints.weighty = 0.0; + addToGridBagPanel(sysInfoPanel, gridBag, constraints, + new JLabel(I18nManager.getText("dialog.about.systeminfo.os") + " : "), + 0, 0); + addToGridBagPanel(sysInfoPanel, gridBag, constraints, + new JLabel(System.getProperty("os.name")), + 1, 0); + addToGridBagPanel(sysInfoPanel, gridBag, constraints, + new JLabel(I18nManager.getText("dialog.about.systeminfo.java") + " : "), + 0, 1); + addToGridBagPanel(sysInfoPanel, gridBag, constraints, + new JLabel(System.getProperty("java.runtime.version")), + 1, 1); + addToGridBagPanel(sysInfoPanel, gridBag, constraints, + new JLabel(I18nManager.getText("dialog.about.systeminfo.java3d") + " : "), + 0, 2); + addToGridBagPanel(sysInfoPanel, gridBag, constraints, + new JLabel(I18nManager.getText(WindowFactory.isJava3dEnabled()?"dialog.about.yes":"dialog.about.no")), + 1, 2); + addToGridBagPanel(sysInfoPanel, gridBag, constraints, + new JLabel(I18nManager.getText("dialog.about.systeminfo.povray") + " : "), + 0, 3); + addToGridBagPanel(sysInfoPanel, gridBag, constraints, + new JLabel(I18nManager.getText(ExternalTools.isPovrayInstalled()?"dialog.about.yes":"dialog.about.no")), + 1, 3); + addToGridBagPanel(sysInfoPanel, gridBag, constraints, + new JLabel(I18nManager.getText("dialog.about.systeminfo.exiftool") + " : "), + 0, 4); + addToGridBagPanel(sysInfoPanel, gridBag, constraints, + new JLabel(I18nManager.getText(ExternalTools.isExiftoolInstalled()?"dialog.about.yes":"dialog.about.no")), + 1, 4); + tabPane.add(I18nManager.getText("dialog.about.systeminfo"), sysInfoPanel); + + // Third pane for credits + JPanel creditsPanel = new JPanel(); + gridBag = new GridBagLayout(); + creditsPanel.setLayout(gridBag); + constraints = new GridBagConstraints(); + constraints.weightx = 0.0; constraints.weighty = 0.0; + + addToGridBagPanel(creditsPanel, gridBag, constraints, + new JLabel(I18nManager.getText("dialog.about.credits.code") + " : "), + 0, 0); + addToGridBagPanel(creditsPanel, gridBag, constraints, + new JLabel("activityworkshop.net"), + 1, 0); + addToGridBagPanel(creditsPanel, gridBag, constraints, + new JLabel(I18nManager.getText("dialog.about.credits.exifcode") + " : "), + 0, 1); + addToGridBagPanel(creditsPanel, gridBag, constraints, + new JLabel("Drew Noakes"), + 1, 1); + addToGridBagPanel(creditsPanel, gridBag, constraints, + new JLabel(I18nManager.getText("dialog.about.credits.icons") + " : "), + 0, 2); + addToGridBagPanel(creditsPanel, gridBag, constraints, + new JLabel("Eclipse"), + 1, 2); + addToGridBagPanel(creditsPanel, gridBag, constraints, + new JLabel(I18nManager.getText("dialog.about.credits.translations") + " : "), + 0, 3); + addToGridBagPanel(creditsPanel, gridBag, constraints, + new JLabel("Open Office, Gpsdrive, Babelfish, Leo"), + 1, 3); + addToGridBagPanel(creditsPanel, gridBag, constraints, + new JLabel(I18nManager.getText("dialog.about.credits.devtools") + " : "), + 0, 4); + addToGridBagPanel(creditsPanel, gridBag, constraints, + new JLabel("Mandriva Linux, Sun Java, Eclipse, Svn, Gimp"), + 1, 4); + addToGridBagPanel(creditsPanel, gridBag, constraints, + new JLabel(I18nManager.getText("dialog.about.credits.othertools") + " : "), + 0, 5); + addToGridBagPanel(creditsPanel, gridBag, constraints, + new JLabel("Garble, Kate, Povray, Inkscape, Google Earth"), + 1, 5); + addToGridBagPanel(creditsPanel, gridBag, constraints, + new JLabel(I18nManager.getText("dialog.about.credits.thanks") + " : "), + 0, 6); + addToGridBagPanel(creditsPanel, gridBag, constraints, + new JLabel("Friends and loved ones, for encouragement and support"), + 1, 6); + tabPane.add(I18nManager.getText("dialog.about.credits"), creditsPanel); + + // OK button at the bottom + JPanel okPanel = new JPanel(); + okPanel.setLayout(new FlowLayout(FlowLayout.CENTER)); + _okButton = new JButton(I18nManager.getText("button.ok")); + _okButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { dispose(); } }); - okButton.setAlignmentX(JButton.CENTER_ALIGNMENT); - mainPanel.add(okButton); + _okButton.addKeyListener(new KeyListener() { + public void keyPressed(KeyEvent e) + { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {dispose();} + } + public void keyTyped(KeyEvent e) {} + public void keyReleased(KeyEvent e) {} + }); + okPanel.add(_okButton); + mainPanel.add(okPanel, BorderLayout.SOUTH); return mainPanel; } + /** + * Helper function to reduce complexity of gui making code + * when adding labels to a GridBagLayout + * @param inPanel panel to add to + * @param inLayout GridBagLayout object + * @param inConstraints GridBagConstraints object + * @param inLabel label to add + * @param inX grid x + * @param inY grid y + */ + private static void addToGridBagPanel(JPanel inPanel, GridBagLayout inLayout, GridBagConstraints inConstraints, + JLabel inLabel, int inX, int inY) + { + // set x and y in constraints + inConstraints.gridx = inX; + inConstraints.gridy = inY; + // set anchor + inConstraints.anchor = (inX == 0?GridBagConstraints.EAST:GridBagConstraints.WEST); + // set constraints to label + inLayout.setConstraints(inLabel, inConstraints); + // add label to panel + inPanel.add(inLabel); + } + /** * Show window @@ -87,5 +229,6 @@ public class AboutScreen extends JDialog pack(); // setSize(300,200); super.show(); + _okButton.requestFocus(); } } diff --git a/tim/prune/gui/DetailsDisplay.java b/tim/prune/gui/DetailsDisplay.java index 765b88b..1c68626 100644 --- a/tim/prune/gui/DetailsDisplay.java +++ b/tim/prune/gui/DetailsDisplay.java @@ -2,31 +2,19 @@ package tim.prune.gui; import java.awt.BorderLayout; import java.awt.Component; -import java.awt.FlowLayout; +import java.awt.Dimension; import java.awt.Font; -import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.awt.event.AdjustmentEvent; -import java.awt.event.AdjustmentListener; import java.text.NumberFormat; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; -import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JLabel; -import javax.swing.JList; import javax.swing.JPanel; -import javax.swing.JScrollBar; -import javax.swing.JScrollPane; -import javax.swing.JTabbedPane; import javax.swing.border.EtchedBorder; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; - -import tim.prune.App; import tim.prune.DataSubscriber; import tim.prune.I18nManager; import tim.prune.data.Altitude; @@ -34,6 +22,7 @@ import tim.prune.data.Coordinate; import tim.prune.data.DataPoint; import tim.prune.data.Distance; import tim.prune.data.IntegerRange; +import tim.prune.data.Photo; import tim.prune.data.Selection; import tim.prune.data.TrackInfo; @@ -43,34 +32,21 @@ import tim.prune.data.TrackInfo; */ public class DetailsDisplay extends GenericDisplay { - // App object to be notified of editing commands - private App _app = null; - - // Track details - private JLabel _trackpointsLabel = null; - private JLabel _filenameLabel = null; // Point details private JLabel _indexLabel = null; private JLabel _latLabel = null, _longLabel = null; private JLabel _altLabel = null, _nameLabel = null; - private JLabel _timeLabel = null, _photoFileLabel = null; - // Scroll bar - private JScrollBar _scroller = null; - private boolean _ignoreScrollEvents = false; - // Button panel - private JButton _startRangeButton = null, _endRangeButton = null; - private JButton _deletePointButton = null, _deleteRangeButton = null; + private JLabel _timeLabel = null; // Range details private JLabel _rangeLabel = null; private JLabel _distanceLabel = null, _durationLabel = null; private JLabel _altRangeLabel = null, _updownLabel = null; - // Photos - private JList _photoList = null; - private PhotoListModel _photoListModel = null; - // Waypoints - private JList _waypointList = null; - private WaypointListModel _waypointListModel = null; + + // Photo details + private JLabel _photoLabel = null; + private PhotoThumbnail _photoThumbnail = null; + // Units private JComboBox _unitsDropdown = null; // Formatter @@ -91,39 +67,20 @@ public class DetailsDisplay extends GenericDisplay private static final String LABEL_RANGE_DESCENT = ", " + I18nManager.getText("details.range.descent") + ": "; private static String LABEL_POINT_ALTITUDE_UNITS = null; private static int LABEL_POINT_ALTITUDE_FORMAT = Altitude.FORMAT_NONE; - // scrollbar interval - private static final int SCROLLBAR_INTERVAL = 50; /** * Constructor - * @param inApp App object for callbacks * @param inTrackInfo Track info object */ - public DetailsDisplay(App inApp, TrackInfo inTrackInfo) + public DetailsDisplay(TrackInfo inTrackInfo) { super(inTrackInfo); - _app = inApp; setLayout(new BorderLayout()); JPanel mainPanel = new JPanel(); mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); mainPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); - // Track details panel - JPanel trackDetailsPanel = new JPanel(); - trackDetailsPanel.setLayout(new BoxLayout(trackDetailsPanel, BoxLayout.Y_AXIS)); - trackDetailsPanel.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3)) - ); - JLabel trackDetailsLabel = new JLabel(I18nManager.getText("details.trackdetails")); - Font biggerFont = trackDetailsLabel.getFont(); - biggerFont = biggerFont.deriveFont(Font.BOLD, biggerFont.getSize2D() + 2.0f); - trackDetailsLabel.setFont(biggerFont); - trackDetailsPanel.add(trackDetailsLabel); - _trackpointsLabel = new JLabel(I18nManager.getText("details.notrack")); - trackDetailsPanel.add(_trackpointsLabel); - _filenameLabel = new JLabel(""); - trackDetailsPanel.add(_filenameLabel); // Point details panel JPanel pointDetailsPanel = new JPanel(); @@ -132,6 +89,8 @@ public class DetailsDisplay extends GenericDisplay BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3)) ); JLabel pointDetailsLabel = new JLabel(I18nManager.getText("details.pointdetails")); + Font biggerFont = pointDetailsLabel.getFont(); + biggerFont = biggerFont.deriveFont(Font.BOLD, biggerFont.getSize2D() + 2.0f); pointDetailsLabel.setFont(biggerFont); pointDetailsPanel.add(pointDetailsLabel); _indexLabel = new JLabel(I18nManager.getText("details.nopointselection")); @@ -144,138 +103,63 @@ public class DetailsDisplay extends GenericDisplay pointDetailsPanel.add(_altLabel); _timeLabel = new JLabel(""); pointDetailsPanel.add(_timeLabel); - _photoFileLabel = new JLabel(""); - pointDetailsPanel.add(_photoFileLabel); _nameLabel = new JLabel(""); pointDetailsPanel.add(_nameLabel); pointDetailsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - // Scroll bar - _scroller = new JScrollBar(JScrollBar.HORIZONTAL, 0, SCROLLBAR_INTERVAL, 0, 100); - _scroller.addAdjustmentListener(new AdjustmentListener() { - public void adjustmentValueChanged(AdjustmentEvent e) - { - selectPoint(e.getValue()); - } - }); - _scroller.setEnabled(false); - - // Button panel - JPanel buttonPanel = new JPanel(); - buttonPanel.setLayout(new GridLayout(2, 2, 3, 3)); - _startRangeButton = new JButton(I18nManager.getText("button.startrange")); - _startRangeButton.addActionListener(new ActionListener() - { - public void actionPerformed(ActionEvent e) - { - _trackInfo.getSelection().selectRangeStart(); - } - }); - _startRangeButton.setEnabled(false); - buttonPanel.add(_startRangeButton); - _endRangeButton = new JButton(I18nManager.getText("button.endrange")); - _endRangeButton.addActionListener(new ActionListener() - { - public void actionPerformed(ActionEvent e) - { - _trackInfo.getSelection().selectRangeEnd(); - } - }); - _endRangeButton.setEnabled(false); - buttonPanel.add(_endRangeButton); - _deletePointButton = new JButton(I18nManager.getText("button.deletepoint")); - _deletePointButton.addActionListener(new ActionListener() - { - public void actionPerformed(ActionEvent e) - { - _app.deleteCurrentPoint(); - } - }); - _deletePointButton.setEnabled(false); - buttonPanel.add(_deletePointButton); - _deleteRangeButton = new JButton(I18nManager.getText("button.deleterange")); - _deleteRangeButton.addActionListener(new ActionListener() - { - public void actionPerformed(ActionEvent e) - { - _app.deleteSelectedRange(); - } - }); - _deleteRangeButton.setEnabled(false); - buttonPanel.add(_deleteRangeButton); - buttonPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - // range details panel - JPanel otherDetailsPanel = new JPanel(); - otherDetailsPanel.setLayout(new BoxLayout(otherDetailsPanel, BoxLayout.Y_AXIS)); - otherDetailsPanel.setBorder(BorderFactory.createCompoundBorder( + JPanel rangeDetailsPanel = new JPanel(); + rangeDetailsPanel.setLayout(new BoxLayout(rangeDetailsPanel, BoxLayout.Y_AXIS)); + rangeDetailsPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3)) ); - - JLabel otherDetailsLabel = new JLabel(I18nManager.getText("details.rangedetails")); - otherDetailsLabel.setFont(biggerFont); - otherDetailsPanel.add(otherDetailsLabel); + JLabel rangeDetailsLabel = new JLabel(I18nManager.getText("details.rangedetails")); + rangeDetailsLabel.setFont(biggerFont); + rangeDetailsPanel.add(rangeDetailsLabel); _rangeLabel = new JLabel(I18nManager.getText("details.norangeselection")); - otherDetailsPanel.add(_rangeLabel); + rangeDetailsPanel.add(_rangeLabel); _distanceLabel = new JLabel(""); - otherDetailsPanel.add(_distanceLabel); + rangeDetailsPanel.add(_distanceLabel); _durationLabel = new JLabel(""); - otherDetailsPanel.add(_durationLabel); + rangeDetailsPanel.add(_durationLabel); _altRangeLabel = new JLabel(""); - otherDetailsPanel.add(_altRangeLabel); + rangeDetailsPanel.add(_altRangeLabel); _updownLabel = new JLabel(""); - otherDetailsPanel.add(_updownLabel); - otherDetailsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + rangeDetailsPanel.add(_updownLabel); + rangeDetailsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - // Add tab panel for waypoints / photos - JPanel waypointsPanel = new JPanel(); - waypointsPanel.setLayout(new BoxLayout(waypointsPanel, BoxLayout.Y_AXIS)); - waypointsPanel.setBorder(BorderFactory.createCompoundBorder( + // range details panel + JPanel photoDetailsPanel = new JPanel(); + photoDetailsPanel.setLayout(new BoxLayout(photoDetailsPanel, BoxLayout.Y_AXIS)); + photoDetailsPanel.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3)) ); - JTabbedPane tabPane = new JTabbedPane(); - _waypointListModel = new WaypointListModel(_trackInfo.getTrack()); - _waypointList = new JList(_waypointListModel); - _waypointList.setVisibleRowCount(5); - _waypointList.addListSelectionListener(new ListSelectionListener() { - public void valueChanged(ListSelectionEvent e) - { - if (!e.getValueIsAdjusting()) selectWaypoint(_waypointList.getSelectedIndex()); - }}); - tabPane.addTab(I18nManager.getText("details.waypointsphotos.waypoints"), new JScrollPane(_waypointList)); - _photoListModel = new PhotoListModel(_trackInfo.getPhotoList()); - _photoList = new JList(_photoListModel); - _photoList.setVisibleRowCount(5); - _photoList.addListSelectionListener(new ListSelectionListener() { - public void valueChanged(ListSelectionEvent e) - { - if (!e.getValueIsAdjusting()) selectPhoto(_photoList.getSelectedIndex()); - }}); - // TODO: Re-add photos list after v2 - // tabPane.addTab(I18nManager.getText("details.waypointsphotos.photos"), new JScrollPane(_photoList)); - tabPane.setAlignmentX(Component.LEFT_ALIGNMENT); - waypointsPanel.add(tabPane); - waypointsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - - // add the slider, point details, and the other details to the main panel - mainPanel.add(buttonPanel); - mainPanel.add(Box.createVerticalStrut(5)); - mainPanel.add(_scroller); - mainPanel.add(Box.createVerticalStrut(5)); - mainPanel.add(trackDetailsPanel); - mainPanel.add(Box.createVerticalStrut(5)); + JLabel photoDetailsLabel = new JLabel(I18nManager.getText("details.photodetails")); + photoDetailsLabel.setFont(biggerFont); + photoDetailsPanel.add(photoDetailsLabel); + _photoLabel = new JLabel(I18nManager.getText("details.nophoto")); + photoDetailsPanel.add(_photoLabel); + _photoThumbnail = new PhotoThumbnail(); + _photoThumbnail.setVisible(false); + _photoThumbnail.setPreferredSize(new Dimension(100, 100)); + photoDetailsPanel.add(_photoThumbnail); + + // add the details panels to the main panel mainPanel.add(pointDetailsPanel); mainPanel.add(Box.createVerticalStrut(5)); - mainPanel.add(otherDetailsPanel); + mainPanel.add(rangeDetailsPanel); + mainPanel.add(Box.createVerticalStrut(5)); + mainPanel.add(photoDetailsPanel); mainPanel.add(Box.createVerticalStrut(5)); - mainPanel.add(waypointsPanel); // add the main panel at the top add(mainPanel, BorderLayout.NORTH); // Add units selection JPanel lowerPanel = new JPanel(); - lowerPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); - lowerPanel.add(new JLabel(I18nManager.getText("details.distanceunits") + ": ")); + lowerPanel.setLayout(new BoxLayout(lowerPanel, BoxLayout.Y_AXIS)); + 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() { @@ -285,85 +169,16 @@ public class DetailsDisplay extends GenericDisplay } }); lowerPanel.add(_unitsDropdown); + _unitsDropdown.setAlignmentX(Component.LEFT_ALIGNMENT); add(lowerPanel, BorderLayout.SOUTH); } - /** - * Select the specified point - * @param inValue value to select - */ - private void selectPoint(int inValue) - { - if (_track != null && !_ignoreScrollEvents) - { - _trackInfo.getSelection().selectPoint(inValue); - } - } - - - /** - * Select the specified photo - * @param inPhotoIndex index of selected photo - */ - private void selectPhoto(int inPhotoIndex) - { - if (_photoListModel.getPhoto(inPhotoIndex) != null) - { - // TODO: Deselect the photo when another point is selected - // TODO: show photo thumbnail - // select associated point, if any - DataPoint point = _photoListModel.getPhoto(inPhotoIndex).getDataPoint(); - if (point != null) - { - _trackInfo.selectPoint(point); - } - } - } - - - /** - * Select the specified waypoint - * @param inWaypointIndex index of selected waypoint - */ - private void selectWaypoint(int inWaypointIndex) - { - if (inWaypointIndex >= 0) - { - _trackInfo.selectPoint(_waypointListModel.getWaypoint(inWaypointIndex)); - } - } - - /** * Notification that Track has been updated */ public void dataUpdated(byte inUpdateType) { - // Update track data - if (_track == null || _track.getNumPoints() <= 0) - { - _trackpointsLabel.setText(I18nManager.getText("details.notrack")); - _filenameLabel.setText(""); - } - else - { - _trackpointsLabel.setText(I18nManager.getText("details.track.points") + ": " - + _track.getNumPoints()); - int numFiles = _trackInfo.getFileInfo().getNumFiles(); - if (numFiles == 1) - { - _filenameLabel.setText(I18nManager.getText("details.track.file") + ": " - + _trackInfo.getFileInfo().getFilename()); - } - else if (numFiles > 1) - { - _filenameLabel.setText(I18nManager.getText("details.track.numfiles") + ": " - + numFiles); - } - else _filenameLabel.setText(""); - } - // Update current point data, if any DataPoint currentPoint = _trackInfo.getCurrentPoint(); Selection selection = _trackInfo.getSelection(); @@ -375,7 +190,6 @@ public class DetailsDisplay extends GenericDisplay _longLabel.setText(""); _altLabel.setText(""); _timeLabel.setText(""); - _photoFileLabel.setText(""); _nameLabel.setText(""); } else @@ -393,13 +207,6 @@ public class DetailsDisplay extends GenericDisplay _timeLabel.setText(LABEL_POINT_TIMESTAMP + currentPoint.getTimestamp().getText()); else _timeLabel.setText(""); - if (currentPoint.getPhoto() != null && currentPoint.getPhoto().getFile() != null) - { - _photoFileLabel.setText(I18nManager.getText("details.photofile") + ": " - + currentPoint.getPhoto().getFile().getName()); - } - else - _photoFileLabel.setText(""); String name = currentPoint.getWaypointName(); if (name != null && !name.equals("")) { @@ -408,30 +215,6 @@ public class DetailsDisplay extends GenericDisplay else _nameLabel.setText(""); } - // Update scroller settings - _ignoreScrollEvents = true; - if (_track == null || _track.getNumPoints() < 2) - { - // careful to avoid event loops here - // _scroller.setValue(0); - _scroller.setEnabled(false); - } - else - { - _scroller.setMaximum(_track.getNumPoints() + SCROLLBAR_INTERVAL); - if (currentPointIndex >= 0) - _scroller.setValue(currentPointIndex); - _scroller.setEnabled(true); - } - _ignoreScrollEvents = false; - - // Update button panel - boolean hasPoint = (_track != null && currentPointIndex >= 0); - _startRangeButton.setEnabled(hasPoint); - _endRangeButton.setEnabled(hasPoint); - _deletePointButton.setEnabled(hasPoint); - _deleteRangeButton.setEnabled(selection.hasRangeSelected()); - // Update range details if (_track == null || !selection.hasRangeSelected()) { @@ -475,37 +258,22 @@ public class DetailsDisplay extends GenericDisplay _updownLabel.setText(""); } } - // update waypoints and photos if necessary - if ((inUpdateType | - (DataSubscriber.DATA_ADDED_OR_REMOVED | DataSubscriber.DATA_EDITED | DataSubscriber.WAYPOINTS_MODIFIED)) > 0) - { - _waypointListModel.fireChanged(); - } - if ((inUpdateType | - (DataSubscriber.DATA_ADDED_OR_REMOVED | DataSubscriber.DATA_EDITED | DataSubscriber.PHOTOS_MODIFIED)) > 0) - { - _photoListModel.fireChanged(); - } - // Deselect selected waypoint if selected point has since changed - if (_waypointList.getSelectedIndex() >= 0) + // show photo details and thumbnail + Photo currentPhoto = _trackInfo.getPhotoList().getPhoto(_trackInfo.getSelection().getCurrentPhotoIndex()); + if (_track == null || ( (currentPoint == null || currentPoint.getPhoto() == null) && currentPhoto == null)) { - if (_trackInfo.getCurrentPoint() == null - || !_waypointListModel.getWaypoint(_waypointList.getSelectedIndex()).equals(_trackInfo.getCurrentPoint())) - { - // point is selected in list but different from current point - deselect - _waypointList.clearSelection(); - } + // no photo, hide details + _photoLabel.setText(I18nManager.getText("details.nophoto")); + _photoThumbnail.setVisible(false); } - // Do the same for the photos - if (_photoList.getSelectedIndex() >= 0) + else { - if (_trackInfo.getCurrentPoint() == null - || !_photoListModel.getPhoto(_photoList.getSelectedIndex()).getDataPoint().equals(_trackInfo.getCurrentPoint())) - { - // photo is selected in list but different from current point - deselect - _photoList.clearSelection(); - } + if (currentPhoto == null) {currentPhoto = currentPoint.getPhoto();} + _photoLabel.setText(I18nManager.getText("details.photofile") + ": " + currentPhoto.getFile().getName()); + _photoThumbnail.setVisible(true); + _photoThumbnail.setPhoto(currentPhoto); } + _photoThumbnail.repaint(); } diff --git a/tim/prune/gui/ImageUtils.java b/tim/prune/gui/ImageUtils.java new file mode 100644 index 0000000..4b46959 --- /dev/null +++ b/tim/prune/gui/ImageUtils.java @@ -0,0 +1,86 @@ +package tim.prune.gui; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.awt.image.ConvolveOp; +import java.awt.image.Kernel; + +import javax.swing.ImageIcon; + +/** + * Class for providing generic image processing functions + */ +public abstract class ImageUtils +{ + private static final float SMOOTH_FACTOR = 0.008f; + private static ConvolveOp CONVOLVER = null; + + /** Static block for initialization */ + static + { + float[] smoothMatrix = { + 0, SMOOTH_FACTOR, 0, + SMOOTH_FACTOR, 1-(SMOOTH_FACTOR*4), SMOOTH_FACTOR, + 0, SMOOTH_FACTOR, 0 + }; + CONVOLVER = new ConvolveOp(new Kernel(3, 3, smoothMatrix)); + } + + + /** + * Create a scaled and smoothed image according to the specified size + * @param inImage image to scale + * @param inWidth width to scale to + * @param inHeight height to scale to + * @return BufferedImage containing scaled result + */ + public static BufferedImage createScaledImage(Image inImage, int inWidth, int inHeight) + { + // create smaller image and force its loading + Image smallerImage = inImage.getScaledInstance(inWidth, inHeight, Image.SCALE_SMOOTH); + Image tempImage = new ImageIcon(smallerImage).getImage(); + tempImage.getWidth(null); + + // create buffered image to do transform + BufferedImage buffer = new BufferedImage(inWidth, inHeight, BufferedImage.TYPE_INT_RGB); + // copy scaled picture into buffer + Graphics buffG = buffer.getGraphics(); + buffG.drawImage(smallerImage, 0, 0, inWidth, inHeight, null); + buffG.dispose(); + + // clear variables + smallerImage = null; tempImage = null; + // smooth scaled image using a normalized 3x3 matrix - taking next neighbour + buffer = CONVOLVER.filter(buffer, null); + + return buffer; + } + + + /** + * Work out the max size of a thumbnail + * @param inOrigWidth width of original picture + * @param inOrigHeight height of original picture + * @param inMaxWidth max width of thumbnail + * @param inMaxHeight max height of thumbnail + * @return size of thumbnail as Dimension + */ + public static Dimension getThumbnailSize(int inOrigWidth, int inOrigHeight, int inMaxWidth, int inMaxHeight) + { + if (inMaxWidth <= 0 || inMaxHeight <= 0) + { + //System.out.println("Can't do it - maxwidth=" + inMaxWidth + ", maxheight=" + inMaxHeight); + return new Dimension(0,0); + } + // work out maximum zoom ratio available so that thumbnail isn't too big + double xZoom = inMaxWidth * 1.0 / inOrigWidth; + double yZoom = inMaxHeight * 1.0 / inOrigHeight; + double zoom = (xZoom > yZoom?yZoom:xZoom); + // Don't make thumbnail bigger than picture + if (zoom > 1.0) {return new Dimension(inOrigWidth, inOrigHeight);} + // calculate new width and height + return new Dimension ((int) (zoom * inOrigWidth), (int) (zoom * inOrigHeight)); + } +} diff --git a/tim/prune/gui/MapChart.java b/tim/prune/gui/MapChart.java index 1236f13..e705f84 100644 --- a/tim/prune/gui/MapChart.java +++ b/tim/prune/gui/MapChart.java @@ -49,7 +49,6 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis private BufferedImage _image = null; private JPopupMenu _popup = null; private JCheckBoxMenuItem _autoPanMenuItem = null; - private String _trackString = null; private int _numPoints = -1; private double _scale; private double _offsetX, _offsetY, _zoomScale; @@ -153,10 +152,14 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis } _lastSelectedPoint = selectedPoint; + // Create background if necessary if (_image == null || width != _image.getWidth() || height != _image.getHeight()) { createBackgroundImage(); } + // return if image has been set to null by other thread + if (_image == null) {return;} + // draw buffered image onto g g.drawImage(_image, 0, 0, width, height, COLOR_BG, null); @@ -208,7 +211,7 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis } // Attempt to grab keyboard focus if possible - this.requestFocus(); + //this.requestFocus(); } @@ -250,8 +253,11 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis { DataPoint point = _track.getPoint(i); String waypointName = point.getWaypointName(); - if (waypointName != null && !waypointName.equals("") && numWaypointNamesShown < LIMIT_WAYPOINT_NAMES) + if (waypointName != null && !waypointName.equals("")) { + // escape if nothing more to do + if (numWaypointNamesShown >= LIMIT_WAYPOINT_NAMES || _image == null) {break;} + // calculate coordinates of point x = halfWidth + (int) ((_track.getX(i) - _offsetX) / _scale * _zoomScale); y = halfHeight - (int) ((_track.getY(i) - _offsetY) / _scale * _zoomScale); if (x > BORDER_WIDTH && x < (width - BORDER_WIDTH) @@ -305,15 +311,22 @@ public class MapChart extends GenericChart implements MouseWheelListener, KeyLis */ private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight) { - // if (true) return true; - for (int x=0; x 0); // set functions which require data _saveItem.setEnabled(hasData); + _saveButton.setEnabled(hasData); _exportKmlItem.setEnabled(hasData); _exportPovItem.setEnabled(hasData); _deleteDuplicatesItem.setEnabled(hasData); @@ -344,12 +492,29 @@ public class MenuManager implements DataSubscriber // is undo available? boolean hasUndo = !_app.getUndoStack().isEmpty(); _undoItem.setEnabled(hasUndo); + _undoButton.setEnabled(hasUndo); _clearUndoItem.setEnabled(hasUndo); // is there a current point? boolean hasPoint = (hasData && _selection.getCurrentPointIndex() >= 0); _editPointItem.setEnabled(hasPoint); + _editPointButton.setEnabled(hasPoint); _editWaypointNameItem.setEnabled(hasPoint); _deletePointItem.setEnabled(hasPoint); + _selectStartItem.setEnabled(hasPoint); + _selectStartButton.setEnabled(hasPoint); + _selectEndItem.setEnabled(hasPoint); + _selectEndButton.setEnabled(hasPoint); + // are there any photos? + _saveExifItem.setEnabled(_photos != null && _photos.getNumPhotos() > 0); + // is there a current photo? + boolean hasPhoto = _photos != null && _photos.getNumPhotos() > 0 + && _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); + _deletePhotoItem.setEnabled(hasPhoto); // is there a current range? boolean hasRange = (hasData && _selection.hasRangeSelected()); _deleteRangeItem.setEnabled(hasRange); diff --git a/tim/prune/gui/PhotoThumbnail.java b/tim/prune/gui/PhotoThumbnail.java new file mode 100644 index 0000000..a8c94eb --- /dev/null +++ b/tim/prune/gui/PhotoThumbnail.java @@ -0,0 +1,123 @@ +package tim.prune.gui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.image.BufferedImage; + +import javax.swing.ImageIcon; +import javax.swing.JPanel; + +import tim.prune.I18nManager; +import tim.prune.data.Photo; + +/** + * GUI component for showing photo thumbnail + */ +public class PhotoThumbnail extends JPanel implements Runnable +{ + private Photo _photo = null; + private BufferedImage _thumbnail = null; + private int _lastWidth = -1; + private int _lastHeight = -1; + private boolean _loadingImage = false; + private static String _loadingString = null; + + + /** + * Constructor + */ + public PhotoThumbnail() + { + // TODO: Make size of thumbnail dynamic, as big as it can be + setOpaque(true); + _loadingString = I18nManager.getText("details.photo.loading") + " ..."; + } + + + /** + * Set the Photo + * @param inPhoto Photo object to show thumbnail for + */ + public void setPhoto(Photo inPhoto) + { + // Check whether the photo has changed + if (_photo == inPhoto) {return;} + _photo = inPhoto; + _thumbnail = null; + } + + + /** + * Override paint method + * @see javax.swing.JComponent#paint(java.awt.Graphics) + */ + public void paint(Graphics inG) + { + super.paint(inG); + if (_photo != null) + { + // recalculate thumbnail if photo has changed + if (_thumbnail == null || getWidth() != _lastWidth || getHeight() != _lastHeight) + { + // initiate load if not already started + if (!_loadingImage) + { + _loadingImage = true; + new Thread(this).start(); + } + } + // Set width and height + _lastWidth = getWidth(); + _lastHeight = getHeight(); + // if loading, display image + if (_loadingImage) + { + inG.setColor(Color.BLACK); + inG.drawString(_loadingString, 10, 30); + } + else + { + // Copy scaled, smoothed image onto the screen + inG.drawImage(_thumbnail, 0, 0, _thumbnail.getWidth(), _thumbnail.getHeight(), null); + } + } + } + + + /** + * Run method, for loading image in separate thread + * @see java.lang.Runnable#run() + */ + public void run() + { + int picWidth = _photo.getWidth(); + int picHeight = _photo.getHeight(); + if (picWidth > -1 && picHeight > -1) + { + 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; + repaint(); + } +} diff --git a/tim/prune/gui/ProfileChart.java b/tim/prune/gui/ProfileChart.java index 431da0d..8bc2c05 100644 --- a/tim/prune/gui/ProfileChart.java +++ b/tim/prune/gui/ProfileChart.java @@ -38,6 +38,7 @@ public class ProfileChart extends GenericChart /** * Override paint method to draw map + * @param g Graphics object */ public void paint(Graphics g) { @@ -51,7 +52,8 @@ public class ProfileChart extends GenericChart int maxAltitude = altitudeRange.getMaximum(); // message if no altitudes in track - if (minAltitude < 0 || maxAltitude < 0) + if (minAltitude < 0 || maxAltitude < 0 + || minAltitude == maxAltitude) { g.setColor(COLOR_LINES); g.drawString(I18nManager.getText("display.noaltitudes"), 50, height/2); diff --git a/tim/prune/gui/SelectorDisplay.java b/tim/prune/gui/SelectorDisplay.java new file mode 100644 index 0000000..38e4165 --- /dev/null +++ b/tim/prune/gui/SelectorDisplay.java @@ -0,0 +1,259 @@ +package tim.prune.gui; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.border.EtchedBorder; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +import tim.prune.DataSubscriber; +import tim.prune.I18nManager; +import tim.prune.data.DataPoint; +import tim.prune.data.Photo; +import tim.prune.data.TrackInfo; + +/** + * Class to allow selection of points and photos + * as a visual component + */ +public class SelectorDisplay extends GenericDisplay +{ + // Track details + private JLabel _trackpointsLabel = null; + private JLabel _filenameLabel = null; + // Scroll bar + private JScrollBar _scroller = null; + private boolean _ignoreScrollEvents = false; + + // Photos + private JList _photoList = null; + private PhotoListModel _photoListModel = null; + // Waypoints + private JList _waypointList = null; + private WaypointListModel _waypointListModel = null; + + // scrollbar interval + private static final int SCROLLBAR_INTERVAL = 50; + // number of rows in lists + private static final int NUM_LIST_ENTRIES = 7; + + + /** + * Constructor + * @param inTrackInfo Track info object + */ + public SelectorDisplay(TrackInfo inTrackInfo) + { + super(inTrackInfo); + setLayout(new BorderLayout()); + + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); + mainPanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); + + // Track details panel + JPanel trackDetailsPanel = new JPanel(); + trackDetailsPanel.setLayout(new BoxLayout(trackDetailsPanel, BoxLayout.Y_AXIS)); + trackDetailsPanel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3)) + ); + JLabel trackDetailsLabel = new JLabel(I18nManager.getText("details.trackdetails")); + Font biggerFont = trackDetailsLabel.getFont(); + biggerFont = biggerFont.deriveFont(Font.BOLD, biggerFont.getSize2D() + 2.0f); + trackDetailsLabel.setFont(biggerFont); + trackDetailsPanel.add(trackDetailsLabel); + _trackpointsLabel = new JLabel(I18nManager.getText("details.notrack")); + trackDetailsPanel.add(_trackpointsLabel); + _filenameLabel = new JLabel(""); + trackDetailsPanel.add(_filenameLabel); + + // Scroll bar + _scroller = new JScrollBar(JScrollBar.HORIZONTAL, 0, SCROLLBAR_INTERVAL, 0, 100); + _scroller.addAdjustmentListener(new AdjustmentListener() { + public void adjustmentValueChanged(AdjustmentEvent e) + { + selectPoint(e.getValue()); + } + }); + _scroller.setEnabled(false); + + // Add panel for waypoints / photos + JPanel listsPanel = new JPanel(); + listsPanel.setLayout(new BoxLayout(listsPanel, BoxLayout.Y_AXIS)); + listsPanel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(3, 3, 3, 3)) + ); + _waypointListModel = new WaypointListModel(_trackInfo.getTrack()); + _waypointList = new JList(_waypointListModel); + _waypointList.setVisibleRowCount(NUM_LIST_ENTRIES); + _waypointList.addListSelectionListener(new ListSelectionListener() { + public void valueChanged(ListSelectionEvent e) + { + if (!e.getValueIsAdjusting()) selectWaypoint(_waypointList.getSelectedIndex()); + }}); + listsPanel.add(new JLabel(I18nManager.getText("details.waypointsphotos.waypoints"))); + listsPanel.add(new JScrollPane(_waypointList)); + _photoListModel = new PhotoListModel(_trackInfo.getPhotoList()); + _photoList = new JList(_photoListModel); + _photoList.setVisibleRowCount(NUM_LIST_ENTRIES); + _photoList.addListSelectionListener(new ListSelectionListener() { + public void valueChanged(ListSelectionEvent e) + { + if (!e.getValueIsAdjusting()) selectPhoto(_photoList.getSelectedIndex()); + }}); + listsPanel.add(new JLabel(I18nManager.getText("details.waypointsphotos.photos"))); + listsPanel.add(new JScrollPane(_photoList)); + listsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + // add the controls to the main panel + mainPanel.add(trackDetailsPanel); + 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); + // set preferred width to be small + setPreferredSize(new Dimension(100, 100)); + } + + + /** + * Select the specified point + * @param inValue value to select + */ + private void selectPoint(int inValue) + { + if (_track != null && !_ignoreScrollEvents) + { + _trackInfo.getSelection().selectPoint(inValue); + } + } + + + /** + * Select the specified photo + * @param inPhotoIndex index of selected photo + */ + private void selectPhoto(int inPhotoIndex) + { + _trackInfo.selectPhoto(inPhotoIndex); + } + + + /** + * Select the specified waypoint + * @param inWaypointIndex index of selected waypoint + */ + private void selectWaypoint(int inWaypointIndex) + { + if (inWaypointIndex >= 0) + { + _trackInfo.selectPoint(_waypointListModel.getWaypoint(inWaypointIndex)); + } + } + + + /** + * Notification that Track has been updated + */ + public void dataUpdated(byte inUpdateType) + { + // Update track data + if (_track == null || _track.getNumPoints() <= 0) + { + _trackpointsLabel.setText(I18nManager.getText("details.notrack")); + _filenameLabel.setText(""); + } + else + { + _trackpointsLabel.setText(I18nManager.getText("details.track.points") + ": " + + _track.getNumPoints()); + int numFiles = _trackInfo.getFileInfo().getNumFiles(); + if (numFiles == 1) + { + _filenameLabel.setText(I18nManager.getText("details.track.file") + ": " + + _trackInfo.getFileInfo().getFilename()); + } + else if (numFiles > 1) + { + _filenameLabel.setText(I18nManager.getText("details.track.numfiles") + ": " + + numFiles); + } + else _filenameLabel.setText(""); + } + + // Update scroller settings + int currentPointIndex = _trackInfo.getSelection().getCurrentPointIndex(); + _ignoreScrollEvents = true; + if (_track == null || _track.getNumPoints() < 2) + { + // careful to avoid event loops here + // _scroller.setValue(0); + _scroller.setEnabled(false); + } + else + { + _scroller.setMaximum(_track.getNumPoints() + SCROLLBAR_INTERVAL); + if (currentPointIndex >= 0) + _scroller.setValue(currentPointIndex); + _scroller.setEnabled(true); + } + _ignoreScrollEvents = false; + + // update waypoints and photos if necessary + if ((inUpdateType | + (DataSubscriber.DATA_ADDED_OR_REMOVED | DataSubscriber.DATA_EDITED | DataSubscriber.WAYPOINTS_MODIFIED)) > 0) + { + _waypointListModel.fireChanged(); + } + if ((inUpdateType | + (DataSubscriber.DATA_ADDED_OR_REMOVED | DataSubscriber.DATA_EDITED | DataSubscriber.PHOTOS_MODIFIED)) > 0) + { + _photoListModel.fireChanged(); + } + // Deselect selected waypoint if selected point has since changed + if (_waypointList.getSelectedIndex() >= 0) + { + if (_trackInfo.getCurrentPoint() == null + || !_waypointListModel.getWaypoint(_waypointList.getSelectedIndex()).equals(_trackInfo.getCurrentPoint())) + { + // point is selected in list but different from current point - deselect + _waypointList.clearSelection(); + } + } + // Do the same for the photos + if (_photoList.getSelectedIndex() >= 0) + { + DataPoint trackPoint = _trackInfo.getCurrentPoint(); + Photo selectedPhoto = _photoListModel.getPhoto(_photoList.getSelectedIndex()); + // Get selected Photo, if it's still there + DataPoint photoPoint = null; + if (selectedPhoto != null) { + photoPoint = _photoListModel.getPhoto(_photoList.getSelectedIndex()).getDataPoint(); + } + // Compare selected photo with selected point + if ( (photoPoint != null && (trackPoint == null || !photoPoint.equals(trackPoint))) + || (_trackInfo.getSelection().getCurrentPhotoIndex() < 0) ) + { + // photo is selected in list but different from current point - deselect + _photoList.clearSelection(); + _trackInfo.getSelection().deselectPhoto(); + } + } + } +} diff --git a/tim/prune/gui/images/add_photo_icon.png b/tim/prune/gui/images/add_photo_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a9d70b89409cce157372357191f95673e1570d91 GIT binary patch literal 339 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbK}LV!<*E08{Kr#-VocwVKLdE0rT zSw_nmrIt2HEo+im*CoGoqVm>B%Dbkk&6qK9_N@N-b9$F8Y>(UVedn5L*Q>63*Hs?e zSbk)4@$s#N=guF!dwtjMyUl-|FaG)4?C{XE)7O>#Ix8Euj_~|_lR@6d@pN$v zk+?kf%xN(P1%ZZ#oR7s0?eM<$U)NOh_^CttUzprq$@pVc(Q}!o>#s_T)c38MvV4lz zG6V5rGVPiSZakAxtLNPkXTDeeNcWknt(}lAkFFBe!=ubMBNwbr4RHD9rFZl~fJgRS egDdYJ_=^kAU>B6?v#tT!%i!ti=d#Wzp$P!#uY_O# literal 0 HcmV?d00001 diff --git a/tim/prune/gui/images/add_textfile_icon.png b/tim/prune/gui/images/add_textfile_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..356239b9c7c8b746e6e0080b71fc5de152ed1bd0 GIT binary patch literal 484 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbMffdHQn*H2%6fBW(0*YCeSfBm`l z;>VLWKcBz*`S#PVJI}x0fBEC#s~@jE{QU9j@3VJ5KYacD{ny{GKYoAz`TO^uzvs>$ zoiStL>{J1!U5n>;&7ae|byeMl<&CSCwEq2a{`l6yBb$p4ZYc5}QZ=I;Tb&~R~>1uzTFP>K^wyaTVS(DV#CaHB@^1tsk&nyxC|NlSGX~ctTr*5PI zRU4KB`2_>@k^~s!XZa}tH8Uo8ySv!iB^#6iIh+L^k;OpT1B~5HX4?T74W2HJArhBk z&)gJhG7xaNC{XSiI5D)NX+}fSv;X_gO_(=j+VkAJQr^q;H{LJQh*@J2x^a<#p~e2w z5nj^k*dJSPTr^3NaNcTe@&5O>pBuVo_15gKF8xt>JJRt3pWr#}4>nv!k7){=(w#Y- wJ3SXwA3%1au37r>mdKI;Vst02!+FFaQ7m literal 0 HcmV?d00001 diff --git a/tim/prune/gui/images/connect_photo_icon.png b/tim/prune/gui/images/connect_photo_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d32d52e764cbc917bf9c5b220effad4482f11b90 GIT binary patch literal 286 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!eSlAhD<>0>#bA(@e%?+yJv}Ki zGchMCzA!Jkz1qjT?Yz+}Bln53dV1TIELrG!)pgg#DX%xL-f;f)ws+fp{x<>&kwe zot;D3q*iRhC!kP-r;B5V#O0}dC&d~RI9wfPc?1Xl{~s;9mGAAVbrs(Y7;6$QcW~Hf zoLQH4r1(jY%L2pgi7y?J*BbF%7f*b@2I|^vX9gL_W;t XmR--<^^x@_$Sn+>u6{1-oD!M{GNn@o~+6#1@$v`9K5*q=#}W4&e*)}*u1Xzf}VuJ z-sF-A$tC^i<&(0jr{>qs%&(tO+%&tSc}{uTyy~t+b-hb>9K5jS=#}H=@1$2u%B-4_ zT{Eq`eSR58c42ML;>HQfnkTL3n6`Syp^MuOUf6cv{N7_%4xPSn?EIblh8cBzOWLQe zX`Zyad)B%o8xAbnbZG648%IOZ9jD3@VVQE&fGY5;m$!2d3(Zw4gbM_VJHB_pDc_F3?2+RAjg8@ zgn@ltgGWFR7HY2~ckq`T8**Dl>A1q~Ms4O0!L>o-jmn&ftx zUDzqo#A0H?rfy;(A)(G@BB9m9ywm8(Ht8lF2UUlLm>Gdd$6A?G8cYO46sB|5x{GiG PxolkI>LS6+$Y2cs9cIBb literal 0 HcmV?d00001 diff --git a/tim/prune/gui/images/save_icon.gif b/tim/prune/gui/images/save_icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..499dd0ca6021c351c641053be30eecb5b4a7e7e8 GIT binary patch literal 639 zcmZ?wbhEHb6krfwc*el+6w~t;=?ud9vy7?cE^q z*1q%4_Md-x_|o&km!6%t_VVPl7w2!kK6mTY)%$O+-hXrZ@w-PaKBPA<&u(2-(6RIc zNcPq9H=oX5zOnP@<=ao*J$&)u*_+SB-Al@Q7r*)NC8Bg+ zz;2_n+r|KMbN2{G6a4_&{kGN)4gvc5juAGR#sRMOu1@w&t^vlzRuN`KMgigX?uQ4k zYgk5@s;SFPcrC52W*VWRCnlyRuKMMRsEVpigru?tONzsU2OVNcZfg!)N^s&3;9>A! zDQIQv=+@exGeO}YGc&uwQjf$B2b)>gB6wUn7Bn7go*uf&AmXGW<1{9LBBLK|iTUf!iNgE-Nru3Nx#zQt~(*zWJFuccynPJ|{ca(_mKgV|* zD5d&3Dy7l@?`JM{^P?5$x+V||kjcKLR;v<oEs literal 0 HcmV?d00001 diff --git a/tim/prune/gui/images/set_start_icon.png b/tim/prune/gui/images/set_start_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a3196ce384abb5eb583eff39278102deb4be4db4 GIT binary patch literal 667 zcmV;M0%ZM(P)o+qn>xPQz3W<$BRi2k)PspM<9HHre{a3(1F-Xiij6EI)U@|cS z7>wgEfyTBYTse7)U@%DbOP<SN86~j7{(tBWAX)#pL@cy{(6*r7cLB312lHXVtrvz07A`&RWjp}OeQe}g8|MQYh&&6 z9IZzVG5`ED?Y+yCB~ux4*&GxM>kEq#PrJm^E~%MmiG(byu!R9Mn#s8Y08_8u(B8Z3 zt1z2-4?s9yka*gquCax>#uguBYFczaqXYgZxoqxt8Fion&?U*0cW*ah&+{+^gGNID zqS4M$89y$$TpYXsKx!uvYcqNlcM}eMCcj1&FoSV zjpkrgxd8O_^;H6S;Kvr$0w@3x@c}YkhOX-|ynru*!9lq&X$ zqKG0QxLKF(JF(&m+u9opws=8aBwi^m_mxPv20IiX7z}Q{SHOyh>@NAr0QW|30mK^W zo1os$tr}R(n9XHBNAC`(KbmP|WN_Oy?&!D-z%Pc_{;6f#aUlQz002ovPDHLkV1g4w B9TWfn literal 0 HcmV?d00001 diff --git a/tim/prune/gui/images/undo_icon.gif b/tim/prune/gui/images/undo_icon.gif new file mode 100644 index 0000000000000000000000000000000000000000..eae118ad1683ddb6ba797d1aa4636b1369c6e1f9 GIT binary patch literal 355 zcmZ?wbhEHb6krfwxXQrr=hw}@f1dsMdGp`j_kVvs`wL|Je)jvr=HDN-{QkJ*_se;| zU(Wyic;dSo-M^ns`~7tK?-%p#oa+AlV*ac9TYleb{e8dl_k+&g4|{(<>OH%o^!MXQ z_pa`|vMv75%Ct-S+pZsEV{QvY)`Y;=1SpR4WhdmMYq<7>}e9) zQ75{(P;h0Q;M!uL)rCSE%7m9?3#`Z$T%IHN|NnmmLO}5+3nK%A34;zuA;?b*Yz+>x z3OsbA`nT|&ELNG=G56$V4=JD4tE&r4B&M`2KI!1hK7fZIWkxS0HZON`v3p{ literal 0 HcmV?d00001 diff --git a/tim/prune/lang/prune-texts.properties b/tim/prune/lang/prune-texts.properties index ff55656..5214018 100644 --- a/tim/prune/lang/prune-texts.properties +++ b/tim/prune/lang/prune-texts.properties @@ -27,6 +27,12 @@ menu.edit.rearrange.nearest=Each to nearest track point menu.select=Select menu.select.all=Select all menu.select.none=Select none +menu.select.start=Set range start +menu.select.end=Set range end +menu.photo=Photo +menu.photo.saveexif=Save to Exif +menu.photo.connect=Connect to point +menu.photo.delete=Remove photo menu.3d=Three-D menu.3d.show3d=Show in Three-D menu.help=Help @@ -42,6 +48,10 @@ dialog.exit.confirm.title=Exit 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=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 @@ -69,6 +79,7 @@ dialog.openoptions.deliminfo.norecords=No records dialog.openoptions.tabledesc=Extract of file dialog.openoptions.altitudeunits=Altitude units dialog.jpegload.subdirectories=Include subdirectories +dialog.jpegload.loadjpegswithoutcoords=Include photos without coordinates dialog.jpegload.progress.title=Loading photos dialog.jpegload.progress=Please wait while the photos are searched dialog.jpegload.title=Loaded photos @@ -89,8 +100,10 @@ dialog.save.ok2=points to file dialog.save.overwrite.title=File already exists 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=Please enter a short description for the data -dialog.exportkml.filetype=KML files +dialog.exportkml.text=Title for the data +dialog.exportkml.kmz=Compress to make kmz file +dialog.exportkml.exportimages=Export image thumbnails to kmz +dialog.exportkml.filetype=KML, KMZ files dialog.exportpov.title=Export POV dialog.exportpov.text=Please enter the parameters for the POV export dialog.exportpov.font=Font @@ -124,6 +137,19 @@ dialog.pointnameedit.name=Waypoint name dialog.pointnameedit.uppercase=UPPER case dialog.pointnameedit.lowercase=lower case dialog.pointnameedit.sentencecase=Sentence case +dialog.saveexif.title=Save Exif +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.status=Status +dialog.saveexif.table.save=Save +dialog.saveexif.photostatus.connected=Connected +dialog.saveexif.photostatus.disconnected=Disconnected +dialog.saveexif.photostatus.modified=Modified +dialog.saveexif.overwrite=Overwrite files +dialog.saveexif.ok1=Saved +dialog.saveexif.ok2=photo files dialog.about.title=About Prune dialog.about.version=Version dialog.about.build=Build @@ -131,10 +157,29 @@ dialog.about.summarytext1=Prune is a program for loading, displaying and editing dialog.about.summarytext2=It is released under the Gnu GPL for free, open, worldwide use and enhancement.
Copying, redistribution and modification are permitted and encouraged
according to the conditions in the included license.txt file. dialog.about.summarytext3=Please see http://activityworkshop.net/ for more information and user guides. dialog.about.translatedby=English text by activityworkshop. +dialog.about.systeminfo=System info +dialog.about.systeminfo.os=Operating System +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.yes=Yes +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 # 3d window dialog.3d.title=Prune Three-d view 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 @@ -145,14 +190,19 @@ button.cancel=Cancel button.overwrite=Overwrite button.moveup=Move up button.movedown=Move down -button.startrange=Set start -button.endrange=Set end button.deletepoint=Delete point button.deleterange=Delete range +button.showlines=Show lines button.edit=Edit button.exit=Exit button.close=Close button.continue=Continue +button.yes=Yes +button.no=No +button.yestoall=Yes to all +button.notoall=No to all +button.selectall=Select all +button.selectnone=Select none # Display components display.nodata=No data loaded @@ -181,6 +231,9 @@ display.range.time.hours=h display.range.time.days=d details.waypointsphotos.waypoints=Waypoints details.waypointsphotos.photos=Photos +details.photodetails=Photo details +details.nophoto=No photo selected +details.photo.loading=Loading # Field names fieldname.latitude=Latitude @@ -219,19 +272,27 @@ undo.load=load data undo.loadphotos=load photos undo.editpoint=edit point undo.deletepoint=delete point +undo.deletephoto=remove photo undo.deleterange=delete range undo.compress=compress track undo.insert=insert points undo.deleteduplicates=delete duplicates undo.reverse=reverse range undo.rearrangewaypoints=rearrange waypoints +undo.connectphoto=connect photo # Error messages error.save.dialogtitle=Error saving data error.save.nodata=No data to save error.save.failed=Failed to save the data to file: +error.saveexif.filenotfound=Failed to find photo file +error.saveexif.cannotoverwrite1=Photo file +error.saveexif.cannotoverwrite2=is read-only and can't be overwritten. Write to copy? error.load.dialogtitle=Error loading data error.load.noread=Cannot read file +error.load.nopoints=No coordinate information found in the file +error.load.unknownxml=Unrecognised xml format: +error.load.othererror=Error reading file: error.jpegload.dialogtitle=Error loading photos error.jpegload.nofilesfound=No files found error.jpegload.nojpegsfound=No jpeg files found diff --git a/tim/prune/lang/prune-texts_de.properties b/tim/prune/lang/prune-texts_de.properties index 98fc57c..3a53f65 100644 --- a/tim/prune/lang/prune-texts_de.properties +++ b/tim/prune/lang/prune-texts_de.properties @@ -27,6 +27,12 @@ menu.edit.rearrange.nearest=Jeder zum n menu.select=Selektieren menu.select.all=Alles selektieren menu.select.none=Nichts selektieren +menu.select.start=Start setzen +menu.select.end=Stopp setzen +menu.photo=Foto +menu.photo.saveexif=Exif Daten speichern +menu.photo.connect=Mit Punkt verbinden +menu.photo.delete=Foto entfernen menu.3d=Drei-D menu.3d.show3d=In drei-D zeigen menu.help=Hilfe @@ -42,6 +48,10 @@ dialog.exit.confirm.title=Prune beenden dialog.exit.confirm.text=Ihre Daten wurden nicht gespeichert. Wollen Sie trotzdem das Programm beenden? dialog.openappend.title=Daten anhängen oder ersetzen dialog.openappend.text=Häng diese Daten zu den aktuellen Daten an? +dialog.deletepoint.title=Punkt löschen +dialog.deletepoint.deletephoto=Foto von diesem Punkt auch löschen? +dialog.deletephoto.title=Photo entfernen +dialog.deletephoto.deletepoint=Punkt von diesem Foto auch löschen? dialog.deleteduplicates.title=Duplikate löschen dialog.deleteduplicates.single.text=Duplikat wurde gelöscht dialog.deleteduplicates.multi.text=Duplikate wurden gelöscht @@ -69,6 +79,7 @@ dialog.openoptions.deliminfo.norecords=Keine Rekords dialog.openoptions.tabledesc=Extrakt von der Datei dialog.openoptions.altitudeunits=Höhe Maßeinheiten dialog.jpegload.subdirectories=Subordnern auch durchsuchen +dialog.jpegload.loadjpegswithoutcoords=Auch Fotos ohne Koordinaten laden dialog.jpegload.progress.title=Fotos werden geladen dialog.jpegload.progress=Bitte warten während die Fotos durchgesucht werden dialog.jpegload.title=Fotos geladen @@ -89,8 +100,10 @@ dialog.save.ok2=Punkte gespeichert nach dialog.save.overwrite.title=Datei existiert dialog.save.overwrite.text=Diese Datei existiert schon. Sind Sie sicher, Sie wollen die Datei überschreiben? dialog.exportkml.title=KML exportieren -dialog.exportkml.text=Kurze Beschreibung von den Daten -dialog.exportkml.filetype=KML Dateien +dialog.exportkml.text=Titel für die Daten +dialog.exportkml.kmz=Daten ins kmz Datei komprimieren +dialog.exportkml.exportimages=Bilder ins kmz exportieren +dialog.exportkml.filetype=KML, KMZ Dateien dialog.exportpov.title=POV exportieren dialog.exportpov.text=Geben Sie die Parameter ein für das POV Export dialog.exportpov.font=Font @@ -124,6 +137,19 @@ dialog.pointnameedit.name=Waypoint Name dialog.pointnameedit.uppercase=GROSS geschrieben dialog.pointnameedit.lowercase=klein geschrieben dialog.pointnameedit.sentencecase=Gemischt geschrieben +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.table.photoname=Foto Name +dialog.saveexif.table.status=Status +dialog.saveexif.table.save=Speichern +dialog.saveexif.photostatus.connected=Verbunden +dialog.saveexif.photostatus.disconnected=Getrennt +dialog.saveexif.photostatus.modified=Modifiziert +dialog.saveexif.overwrite=Dateien überschreiben +dialog.saveexif.ok1=Es wurden +dialog.saveexif.ok2=Foto Dateien geschrieben dialog.about.title=Über Prune dialog.about.version=Version dialog.about.build=Build @@ -131,10 +157,29 @@ dialog.about.summarytext1=Prune ist ein Programm f dialog.about.summarytext2=Es ist unter den Gnu GPL zur Verfügung gestellt, für frei, gratis und offen Gebrauch und Weiterentwicklung.
Kopieren, Weiterverbreitung und Veränderungen sind erlaubt und willkommen
unter die Bedingungen in der enthaltenen license.txt Datei. dialog.about.summarytext3=Bitte sehen Sie http://activityworkshop.net/ für weitere Information und Benutzeranleitungen. dialog.about.translatedby=Deutsche Übersetzung von activityworkshop. +dialog.about.systeminfo=System Information +dialog.about.systeminfo.os=Betriebsystem +dialog.about.systeminfo.java=Java Runtime +dialog.about.systeminfo.java3d=Java3d installiert +dialog.about.systeminfo.povray=Povray installiert +dialog.about.systeminfo.exiftool=Exiftool installiert +dialog.about.yes=Ja +dialog.about.no=Nein +dialog.about.credits=Credits +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.translations=Übersetzungen mit Hilfe von +dialog.about.credits.devtools=Entwicklungsprogrammen +dialog.about.credits.othertools=Andere Programmen +dialog.about.credits.thanks=Danke an # 3d window dialog.3d.title=Prune Drei-D Ansicht dialog.3d.altitudecap=Minimum Höhenskala +dialog.3dlines.title=Prune Gitterlinien +dialog.3dlines.empty=Keine Linien zum anzeigen! +dialog.3dlines.intro=Hier sind die Linien für die drei-D Ansicht # Buttons button.ok=OK @@ -145,14 +190,19 @@ button.cancel=Abbrechen button.overwrite=Überschreiben button.moveup=Aufwärts moven button.movedown=Abwärts moven -button.startrange=Start setzen -button.endrange=Stopp setzen button.deletepoint=Punkt löschen button.deleterange=Spanne löschen +button.showlines=Linien anzeigen button.edit=Bearbeiten button.exit=Beenden button.close=Schließen button.continue=Fortsetzen +button.yes=Ja +button.no=Nein +button.yestoall=Ja für alle +button.notoall=Nein für alle +button.selectall=Alle selektieren +button.selectnone=Nichts selektieren # Display components display.nodata=Keine Daten geladen @@ -181,6 +231,9 @@ display.range.time.hours=Std display.range.time.days=T details.waypointsphotos.waypoints=Waypoints details.waypointsphotos.photos=Fotos +details.photodetails=Details vom Foto +details.nophoto=Kein Foto selektiert +details.photo.loading=Laden # Field names fieldname.latitude=Breitengrad @@ -219,19 +272,27 @@ undo.load=Daten laden undo.loadphotos=Fotos laden undo.editpoint=Punkt bearbeiten undo.deletepoint=Punkt löschen +undo.deletephoto=Photo entfernen undo.deleterange=Spanne löschen undo.compress=Track komprimieren undo.insert=Punkte hinzufügen undo.deleteduplicates=Duplikaten löschen undo.reverse=Spanne umdrehen undo.rearrangewaypoints=Waypoints reorganisieren +undo.connectphoto=Foto verbinden # Error messages error.save.dialogtitle=Fehler beim Speichern error.save.nodata=Keine Daten wurden geladen -error.save.failed=Speichern von der Datei fehlgeschlagen : +error.save.failed=Speichern von der Datei fehlgeschlagen: +error.saveexif.filenotfound=Foto Datei nicht gefunden +error.saveexif.cannotoverwrite1=Foto Datei +error.saveexif.cannotoverwrite2=ist schreib-geschützt. Speichern zu einer Kopie? error.load.dialogtitle=Fehler beim Laden error.load.noread=Datei konnte nicht gelesen werden +error.load.nopoints=Keine gültigen Daten in Datei gefunden +error.load.unknownxml=Unbekanntes xml Format: +error.load.othererror=Fehler beim Lesen von der Datei: error.jpegload.dialogtitle=Fehler beim Laden von Fotos error.jpegload.nofilesfound=Keine Dateien gefunden error.jpegload.nojpegsfound=Keine Jpeg Dateien gefunden diff --git a/tim/prune/lang/prune-texts_de_CH.properties b/tim/prune/lang/prune-texts_de_CH.properties index e48b3b7..7b2013e 100644 --- a/tim/prune/lang/prune-texts_de_CH.properties +++ b/tim/prune/lang/prune-texts_de_CH.properties @@ -27,6 +27,12 @@ menu.edit.rearrange.nearest=Jede zum n menu.select=Selektiere menu.select.all=Alles selektiere menu.select.none=Nüüt selektiere +menu.select.start=Start setzä +menu.select.end=Stopp setzä +menu.photo=Föteli +menu.photo.saveexif=Exif Date speicherä +menu.photo.connect=Mitem Punkt verbindä +menu.photo.delete=Föteli entfernä menu.3d=Drüü-D menu.3d.show3d=In drüü-D zeigä menu.help=Hilfe @@ -42,6 +48,10 @@ dialog.exit.confirm.title=Prune be dialog.exit.confirm.text=Ihri Date sind nonig gspeicheret worde. Wend Sie trotzdem s Programm beände? dialog.openappend.title=Date aahänge oder ersätze dialog.openappend.text=Häng diese Date zur aktuelli Daten aa? +dialog.deletepoint.title=Punkt löschä +dialog.deletepoint.deletephoto=s Föteli vonem Punkt au löschä? +dialog.deletephoto.title=Föteli entfernä +dialog.deletephoto.deletepoint=Punkt vonem Föteli au löschä? dialog.deleteduplicates.title=Duplikaten lösche dialog.deleteduplicates.single.text=Duplikat isch glöscht worde dialog.deleteduplicates.multi.text=Duplikaten sin glöscht worde @@ -69,28 +79,31 @@ dialog.openoptions.deliminfo.norecords=Kei Rekords dialog.openoptions.tabledesc=Extrakt vom File dialog.openoptions.altitudeunits=Höchi Masseiheite dialog.jpegload.subdirectories=Subordnern au +dialog.jpegload.loadjpegswithoutcoords=Au Fötelis ohni Koordinate dialog.jpegload.progress.title=Fötelis lade -dialog.jpegload.progress=Bitte warte während die Fötolis durägsucht werde +dialog.jpegload.progress=Bitte warte während die Fötelis durägsucht werde dialog.jpegload.title=Fötelis glade worde dialog.jpegload.photoadded=Föteli isch glade worde dialog.jpegload.photosadded=Fötelis sin glade worde -dialog.saveoptions.title=File speichere -dialog.save.fieldstosave=Fälder zu speichere +dialog.saveoptions.title=File speicherä +dialog.save.fieldstosave=Fälder zu speicherä dialog.save.table.field=Fäld dialog.save.table.hasdata=Het Date -dialog.save.table.save=Speichere -dialog.save.headerrow=Titel Ziile speichere +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.ok1=Es isch -dialog.save.ok2=Punkte gespeichert worde na +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 exportiere -dialog.exportkml.text=Kurze Beschriibig von den Date -dialog.exportkml.filetype=KML Dateie +dialog.exportkml.title=KML exportierä +dialog.exportkml.text=Titel für die Date +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.exportpov.text=Gäbet Sie die Parameter ii fürs POV Export dialog.exportpov.font=Font @@ -124,6 +137,19 @@ dialog.pointnameedit.name=Waypoint Name dialog.pointnameedit.uppercase=GROSS gschriebe dialog.pointnameedit.lowercase=chli gschriebe dialog.pointnameedit.sentencecase=Gmischt gschriebe +dialog.saveexif.title=Exif go speicherä +dialog.saveexif.intro=Wählet Sie die Fötelis uus zum speicherä +dialog.saveexif.nothingtosave=Koordinaten sin nöd geänderet, nüüt zum speicherä +dialog.saveexif.noexiftool=Kei exiftool Programm gfunde. Wiiter? +dialog.saveexif.table.photoname=Föteli Name +dialog.saveexif.table.status=Status +dialog.saveexif.table.save=Speicherä +dialog.saveexif.photostatus.connected=Verbundä +dialog.saveexif.photostatus.disconnected=Gtrännt +dialog.saveexif.photostatus.modified=Gänderet +dialog.saveexif.overwrite=Files überschriebä +dialog.saveexif.ok1=Es sin +dialog.saveexif.ok2=Fötelis gschriebe worde dialog.about.title=Über Prune dialog.about.version=Version dialog.about.build=Build @@ -131,28 +157,52 @@ dialog.about.summarytext1=Prune isch n Programm f dialog.about.summarytext2=Es isch unter den Gnu GPL zur Verfüegig gstellt,für frei, gratis und offen Gebruuch und Wiiterentwicklig.
Kopiere, Wiiterverbreitig und Veränderige sin erlaubt und willkommen
unter die Bedingunge im enthaltene license.txt File. dialog.about.summarytext3=Bitte lueg na http://activityworkshop.net/ für wiitere Information und Benutzeraaleitige. dialog.about.translatedby=Schwiizerdüütschi Übersetzig vo activityworkshop. +dialog.about.systeminfo=Syschtem Info +dialog.about.systeminfo.os=Betriebsyschtem +dialog.about.systeminfo.java=Version vonem Java +dialog.about.systeminfo.java3d=Java3d inschtalliert +dialog.about.systeminfo.povray=Povray inschtalliert +dialog.about.systeminfo.exiftool=Exiftool inschtalliert +dialog.about.yes=Ja +dialog.about.no=Nei +dialog.about.credits=Credits +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.translations=Übersetzige mit dr Hilfe vo +dialog.about.credits.devtools=Entwicklungswärkzüüge +dialog.about.credits.othertools=Anderi Wärkzüüge +dialog.about.credits.thanks=Danke an # 3d window -dialog.3d.title=Prune Drüü-d aasicht +dialog.3d.title=Prune Drüü-d Aasicht dialog.3d.altitudecap=Minimum Höhenskala +dialog.3dlines.title=Prune Gitterlinie +dialog.3dlines.empty=Kei Linie zum aazeigä! +dialog.3dlines.intro=Hier sin die Linie für die drüü-D Aasicht # Buttons button.ok=OK button.back=Zrugg -button.next=Nöchste +button.next=Nöchstä button.finish=Fertig -button.cancel=Abbräche -button.overwrite=Überschriibe -button.moveup=Uufwärts move -button.movedown=Abwärts move -button.startrange=Start setze -button.endrange=Stopp setze -button.deletepoint=Punkt lösche -button.deleterange=Spanne lösche -button.edit=Editiere -button.exit=Beände -button.close=Schliesse -button.continue=Fortsetze +button.cancel=Abbrächä +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.close=Schliessä +button.continue=Fortsetzä +button.yes=Ja +button.no=Nei +button.yestoall=Ja für alli +button.notoall=Nei für alli +button.selectall=Alli selektierä +button.selectnone=Nüüt selektierä # Display components display.nodata=Kei Date glade worde @@ -181,6 +231,9 @@ display.range.time.hours=Std display.range.time.days=T details.waypointsphotos.waypoints=Waypoints details.waypointsphotos.photos=Fötelis +details.photodetails=Details vom Föteli +details.nophoto=Kei föteli selektiert +details.photo.loading=Ladä # Field names fieldname.latitude=Breitegrad @@ -215,23 +268,31 @@ cardinal.e=O cardinal.w=W # Undo operations -undo.load=Date lade -undo.loadphotos=Fötelis lade -undo.editpoint=Punkt editiere -undo.deletepoint=Punkt lösche -undo.deleterange=Spanne lösche -undo.compress=Track komprimiere -undo.insert=Punkte innätue -undo.deleteduplicates=Duplikaten lösche +undo.load=Date ladä +undo.loadphotos=Fötelis ladä +undo.editpoint=Punkt editierä +undo.deletepoint=Punkt löschä +undo.deletephoto=Föteli entfärnä +undo.deleterange=Spanne löschä +undo.compress=Track komprimierä +undo.insert=Punkte innätuä +undo.deleteduplicates=Duplikaten löschä undo.reverse=Spanne umdrähie -undo.rearrangewaypoints=Waypoints reorganisiere +undo.rearrangewaypoints=Waypoints reorganisierä +undo.connectphoto=Föteli verbindä # Error messages error.save.dialogtitle=Fehler 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.noread=File cha nöd glase werde +error.load.nopoints=Kei gültigi Information im Datei gfunde +error.load.unknownxml=Unbekanntes xml Format: +error.load.othererror=Fehler bim Läse: error.jpegload.dialogtitle=Fehler bim Lade von Fötelis error.jpegload.nofilesfound=Kei Dateie gfunde error.jpegload.nojpegsfound=Kei Jpegs gfunde diff --git a/tim/prune/lang/prune-texts_es.properties b/tim/prune/lang/prune-texts_es.properties index 3d797ad..fcd83cf 100644 --- a/tim/prune/lang/prune-texts_es.properties +++ b/tim/prune/lang/prune-texts_es.properties @@ -27,6 +27,12 @@ menu.edit.rearrange.nearest=Ir al m menu.select=Seleccionar menu.select.all=Seleccionar todo menu.select.none=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.delete=Eliminar foto menu.3d=3-D menu.3d.show3d=Mostrar en 3-D menu.help=Ayuda @@ -42,6 +48,10 @@ dialog.exit.confirm.title=Salir de Prune dialog.exit.confirm.text=Los datos han sido modificados. Desea 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.deletephoto.title=Borrar foto +dialog.deletephoto.deletepoint=Borrar punto tambien? dialog.deleteduplicates.title=Borrar duplicados dialog.deleteduplicates.single.text=duplicado eliminado dialog.deleteduplicates.multi.text=duplicados eliminados @@ -69,6 +79,7 @@ dialog.openoptions.deliminfo.norecords=Ningun dato dialog.openoptions.tabledesc=Extraer archivo dialog.openoptions.altitudeunits=Unidades altitud dialog.jpegload.subdirectories=Incluir subdirectorios +dialog.jpegload.loadjpegswithoutcoords=Fotos sin coordenadas tambien dialog.jpegload.progress.title=Cargando fotos dialog.jpegload.progress=Por favor espere mientras se buscan las fotos dialog.jpegload.title=Fotos cargadas @@ -89,8 +100,10 @@ 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=Introduzca breve descripción para los datos -dialog.exportkml.filetype=Archivos KML +dialog.exportkml.text=Descripción para los datos +dialog.exportkml.kmz=Comprimir al archivo kmz +dialog.exportkml.exportimages=Exportar fotos al kmz +dialog.exportkml.filetype=Archivos KML, KMZ dialog.exportpov.title=Exportar POV dialog.exportpov.text=Introdzca los Parametros para exportar dialog.exportpov.font=Fuente @@ -98,7 +111,7 @@ dialog.exportpov.camerax=Camera X dialog.exportpov.cameray=Camera Y dialog.exportpov.cameraz=Camera Z dialog.exportpov.filetype=Archivos POV -dialog.exportpov.warningtracksize=This track has a large number of points, which Java3D might not be able to display.\nAre you sure you want to continue? +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.confirmreversetrack.text=Este track contiene información sobre la fecha, que estará fuera de secuencia después de la inversión. Esta seguro que desea invertir esta sección? dialog.interpolate.title=Interpolar puntos @@ -124,17 +137,49 @@ dialog.pointnameedit.name=Nombre de waypoint dialog.pointnameedit.uppercase=Maysculas dialog.pointnameedit.lowercase=minsculas 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.noexiftool=exiftool program no encontrado. Desea continuar? +dialog.saveexif.table.photoname=Nombre de foto +dialog.saveexif.table.status=Status +dialog.saveexif.table.save=Guardar +dialog.saveexif.photostatus.connected=Connected +dialog.saveexif.photostatus.disconnected=Disconnected +dialog.saveexif.photostatus.modified=Modificado +dialog.saveexif.overwrite=Sobreescribirlar archivos? +dialog.saveexif.ok1=Guardando +dialog.saveexif.ok2=fotos dialog.about.title=Acerca de Prune dialog.about.version=Versión dialog.about.build=Construir dialog.about.summarytext1=Prune es un programa para cargar, mostrar y editar datos de receptores GPS. dialog.about.summarytext2=Distribuido bajo el GNU GPL para uso libre y gratuito.
Se permite (y se anima) la copia, redistribución y modificación de acuerdo
a las condiciones incluidas en el archivo licence.txt. dialog.about.summarytext3=Por favor, ver http://activityworkshop.net/ para más información y guías del usuario. -dialog.about.translatedby=Traducción en español por activityworkshop y un alma muy gentil! +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.java=Java Runtime +dialog.about.systeminfo.java3d=Java3d installed +dialog.about.systeminfo.povray=Povray installed +dialog.about.systeminfo.exiftool=Exiftool installed +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 # 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 # Buttons button.ok=Aceptar @@ -145,14 +190,19 @@ button.cancel=Cancelar button.overwrite=Sobreescribir button.moveup=Mover hacia arriba button.movedown=Mover hacia abajo -button.startrange=Fijar comienzo -button.endrange=Fijar final button.deletepoint=Eliminar punto button.deleterange=Eliminar rango +button.showlines=Mostrar gridlines button.edit=Editar button.exit=Salir button.close=Cerrar button.continue=Continúe +button.yes=Si +button.no=No +button.yestoall=Si por todo +button.notoall=No por todo +button.selectall=Seleccionar todo +button.selectnone=Seleccionar nada # Display components display.nodata=Ningún dato cargado @@ -181,6 +231,9 @@ display.range.time.hours=h display.range.time.days=d details.waypointsphotos.waypoints=Waypoints details.waypointsphotos.photos=Fotos +details.photodetails=Detalles del Foto +details.nophoto=Ningún foto seleccionado +details.photo.loading=Cargar # Field names fieldname.latitude=Latitud @@ -219,19 +272,27 @@ undo.load=cargar datos undo.loadphotos=cargar fotos undo.editpoint=editar punto undo.deletepoint=eliminar punto +undo.deletephoto=eliminar foto undo.deleterange=eliminar rango undo.compress=comprimir track undo.insert=insertar puntos undo.deleteduplicates=eliminar duplicados undo.reverse=invertir rango undo.rearrangewaypoints=reordenar waypoints +undo.connectphoto=connectar foto # Error messages error.save.dialogtitle=Fallo al guardar datos error.save.nodata=Ningún dato salvado error.save.failed=Fallo al guardar datos al archivo: +error.saveexif.filenotfound=Archivo no encontrado +error.saveexif.cannotoverwrite1=No se puede guardar +error.saveexif.cannotoverwrite2=. Guardar a una copia? 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.othererror=Fallo al cargar datos: error.jpegload.dialogtitle=Error cargando fotos error.jpegload.nofilesfound=Ningún archivo encontrado error.jpegload.nojpegsfound=Ningún archivo jpeg encontrado diff --git a/tim/prune/lang/prune-texts_fr.properties b/tim/prune/lang/prune-texts_fr.properties new file mode 100644 index 0000000..59e070b --- /dev/null +++ b/tim/prune/lang/prune-texts_fr.properties @@ -0,0 +1,309 @@ +# Text entries for the Prune application +# French entries as extra + +# Menu entries +menu.file=Fichier +menu.file.open=Ouvrir +menu.file.addphotos=Ouvrir photos +menu.file.save=Enregistrer +menu.file.exportkml=Exporter au KML +menu.file.exportpov=Exporter au POV +menu.file.exit=Quitter +menu.edit=Édition +menu.edit.undo=Undo +menu.edit.clearundo=Purger undo liste +menu.edit.editpoint=Editer point +menu.edit.editwaypointname=Editer nom du waypoint +menu.edit.deletepoint=Supprimer du point +menu.edit.deleterange=Supprimer de range +menu.edit.deleteduplicates=Supprimer des duplicates +menu.edit.compress=Compacter track +menu.edit.interpolate=Interpolate +menu.edit.reverse=Reverse range +menu.edit.rearrange=Rearrange waypoints +menu.edit.rearrange.start=Tous à tête de fichier +menu.edit.rearrange.end=Tous à pied de fichier +menu.edit.rearrange.nearest=Chaque à prochain point +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.photo=Photo +menu.photo.saveexif=Enregistrer à Exif +menu.photo.connect=Connect to point +menu.photo.delete=Remove photo +menu.3d=Trois-D +menu.3d.show3d=Montrer en Trois-D +menu.help=Aide +menu.help.about=À propos de Prune +# Popup menu for map +menu.map.zoomin=Zoom avant +menu.map.zoomout=Zoom arrière +menu.map.zoomfull=Zoom to full scale +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.deleteduplicates.nonefound=No duplicates found +dialog.compresstrack.title=Compress 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.openoptions.filesnippet=Extract of fichier +dialog.load.table.field=Field +dialog.load.table.datatype=Data Type +dialog.load.table.description=Description +dialog.delimiter.label=Séparateur de texte +dialog.delimiter.comma=Virgule , +dialog.delimiter.tab=Tabulation +dialog.delimiter.space=Espace +dialog.delimiter.semicolon=Point-virgule ; +dialog.delimiter.other=Autres +dialog.openoptions.deliminfo.records=records, avec +dialog.openoptions.deliminfo.fields=fields +dialog.openoptions.deliminfo.norecords=Pas de records +dialog.openoptions.tabledesc=Extract of fichier +dialog.openoptions.altitudeunits=Unités de altitude +dialog.jpegload.subdirectories=Subdirectories aussi +dialog.jpegload.loadjpegswithoutcoords=Photos sans coordonnées aussi +dialog.jpegload.progress.title=Loading photos +dialog.jpegload.progress=Please wait while the photos are searched +dialog.jpegload.title=Loaded photos +dialog.jpegload.photoadded=photo was added +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.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.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.filetype=Classeur KML, KMZ +dialog.exportpov.title=Exporter au POV +dialog.exportpov.text=Please enter the parameters for the POV export +dialog.exportpov.font=Police +dialog.exportpov.camerax=Camera X +dialog.exportpov.cameray=Camera Y +dialog.exportpov.cameraz=Camera Z +dialog.exportpov.filetype=Classeur POV +dialog.exportpov.warningtracksize=This track has a large number of points, which Java3D might not be able to display.\nAre you sure you want to continue? +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=Interpolate points +dialog.interpolate.parameter.text=Number of points to insert between selected points +dialog.undo.title=Undo 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=Clear undo list +dialog.clearundo.text=Are you sure you want to clear the undo list?\nAll undo information will be lost! +dialog.pointedit.title=Edit point +dialog.pointedit.text=Select each field to edit and use the 'Edit' button to change the value +dialog.pointedit.table.field=Field +dialog.pointedit.table.value=Value +dialog.pointedit.table.changed=Changed +dialog.pointedit.changevalue.text=Enter the new value for this field +dialog.pointedit.changevalue.title=Edit field +dialog.pointnameedit.title=Edit waypoint name +dialog.pointnameedit.name=Waypoint name +dialog.pointnameedit.uppercase=CASSE MAJUSCULES +dialog.pointnameedit.lowercase=casse minuscules +dialog.pointnameedit.sentencecase=Casse Sentence +dialog.saveexif.title=Save Exif +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.status=Status +dialog.saveexif.table.save=Save +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.about.title=À propos de Prune +dialog.about.version=Version +dialog.about.build=Build +dialog.about.summarytext1=Prune est une programme 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.
Copying, redistribution and modification are permitted and encouraged
according to the conditions in the included license.txt file. +dialog.about.summarytext3=Consultez la page http://activityworkshop.net/ pour de plus détails et user guides. +dialog.about.translatedby=Texte en français de activityworkshop. +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.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.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 + +# 3d window +dialog.3d.title=Vue Trois-d de Prune +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=Retour +button.next=Prochain +button.finish=Fini +button.cancel=Annuler +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=Éditer +button.exit=Terminer +button.close=Fermer +button.continue=Continuer +button.yes=Oui +button.no=Non +button.yestoall=Oui pour tous +button.notoall=Non pour tous +button.selectall=Sélecter tous +button.selectnone=Sélecter rien + +# Display components +display.nodata=No data loaded +display.noaltitudes=Track data does not include altitudes +details.trackdetails=Track details +details.notrack=No track loaded +details.track.points=Points +details.track.file=File +details.track.numfiles=Number of fichiers +details.pointdetails=Point details +details.index.selected=Index +details.index.of=de +details.nopointselection=No point selected +details.photofile=Photo fichier +details.norangeselection=No range selected +details.rangedetails=Range details +details.range.selected=Selected +details.range.to=à +details.altitude.to=à +details.range.climb=Montée +details.range.descent=Descente +details.distanceunits=Unités de distance +display.range.time.secs=s +display.range.time.mins=m +display.range.time.hours=h +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 + +# Field names +fieldname.latitude=Latitude +fieldname.longitude=Longitude +fieldname.altitude=Altitude +fieldname.timestamp=Timestamp +fieldname.waypointname=Nom +fieldname.waypointtype=Type +fieldname.newsegment=Segment +fieldname.custom=Custom +fieldname.prefix=Champ +fieldname.distance=Distance +fieldname.duration=Durée + +# Measurement units +units.metres=mètres +units.metres.short=m +units.feet=pieds +units.feet.short=p +units.kilometres=Kilomètres +units.kilometres.short=km +units.miles=lieues +units.miles.short=li +units.degminsec=Deg-min-sec +units.degmin=Deg-min +units.deg=Degrés + +# Cardinals for 3d plots +cardinal.n=N +cardinal.s=S +cardinal.e=E +cardinal.w=O + +# Undo operations +undo.load=load data +undo.loadphotos=load photos +undo.editpoint=editer point +undo.deletepoint=delete point +undo.deletephoto=remove photo +undo.deleterange=delete range +undo.compress=compress track +undo.insert=insert points +undo.deleteduplicates=delete duplicates +undo.reverse=reverse range +undo.rearrangewaypoints=rearrange waypoints +undo.connectphoto=connect photo + +# Error messages +error.save.dialogtitle=Error saving data +error.save.nodata=No data to save +error.save.failed=Failed to save the data to fichier: +error.saveexif.filenotfound=Failed to find photo fichier +error.saveexif.cannotoverwrite1=Photo fichier +error.saveexif.cannotoverwrite2=is read-only and can't be overwritten. Write to copy? +error.load.dialogtitle=Error loading data +error.load.noread=Cannot read fichier +error.load.nopoints=No coordinate information found in the fichier +error.load.unknownxml=Unrecognised xml format: +error.load.othererror=Error reading fichier: +error.jpegload.dialogtitle=Error loading photos +error.jpegload.nofilesfound=No fichiers found +error.jpegload.nojpegsfound=No jpeg fichiers 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=Desoler, 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=Error in 3d display +error.3d=An error occurred with the 3d display diff --git a/tim/prune/load/FileCacher.java b/tim/prune/load/FileCacher.java index 733754b..ad0f0ef 100644 --- a/tim/prune/load/FileCacher.java +++ b/tim/prune/load/FileCacher.java @@ -96,10 +96,15 @@ public class FileCacher { for (int i=0; i inMaxWidth) - result[i] = result[i].trim(); - if (result[i].length() > inMaxWidth) - result[i] = result[i].substring(0, inMaxWidth); + if (result[i] == null) + result[i] = ""; + else + { + if (result[i].length() > inMaxWidth) + result[i] = result[i].trim(); + if (result[i].length() > inMaxWidth) + result[i] = result[i].substring(0, inMaxWidth); + } } } return result; diff --git a/tim/prune/load/FileLoader.java b/tim/prune/load/FileLoader.java index c7126f5..c1bf3df 100644 --- a/tim/prune/load/FileLoader.java +++ b/tim/prune/load/FileLoader.java @@ -1,91 +1,26 @@ package tim.prune.load; -import java.awt.BorderLayout; -import java.awt.CardLayout; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.GridLayout; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import javax.swing.table.TableCellEditor; - import java.io.File; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JOptionPane; + import tim.prune.App; import tim.prune.I18nManager; -import tim.prune.data.Altitude; -import tim.prune.data.Field; +import tim.prune.load.xml.XmlFileLoader; /** - * Special class to handle file loading including GUI options, - * and conversion to a Track object + * Generic FileLoader class to select a file + * and pass handling on to appropriate loader */ public class FileLoader { - private File _file = null; - private App _app = null; - private JFrame _parentFrame = null; - private JDialog _dialog = null; - private JPanel _cardPanel = null; - private CardLayout _layout = null; - private JButton _backButton = null, _nextButton = null; - private JButton _finishButton = null; - private JButton _moveUpButton = null, _moveDownButton = null; - private JRadioButton[] _delimiterRadios = null; - private JTextField _otherDelimiterText = null; - private JLabel _statusLabel = null; - private DelimiterInfo[] _delimiterInfos = null; private JFileChooser _fileChooser = null; - private FileCacher _fileCacher = null; - private JList _snippetBox = null; - private FileExtractTableModel _fileExtractTableModel = null; - private JTable _fieldTable; - private FieldSelectionTableModel _fieldTableModel = null; - private JComboBox _unitsDropDown = null; - private int _selectedField = -1; - private char _currentDelimiter = ','; - - // previously selected values - private char _lastUsedDelimiter = ','; - private int _lastNumFields = -1; - private Field[] _lastSelectedFields = null; - private int _lastAltitudeFormat = Altitude.FORMAT_NONE; - - // constants - private static final int SNIPPET_SIZE = 6; - private static final int MAX_SNIPPET_WIDTH = 80; - private static final char[] DELIMITERS = {',', '\t', ';', ' '}; - - - /** - * Inner class to listen for delimiter change operations - */ - private class DelimListener implements ActionListener, DocumentListener - { - public void actionPerformed(ActionEvent e) - { - informDelimiterSelected(); - } - public void changedUpdate(DocumentEvent e) - { - informDelimiterSelected(); - } - public void insertUpdate(DocumentEvent e) - { - informDelimiterSelected(); - } - public void removeUpdate(DocumentEvent e) - { - informDelimiterSelected(); - } - } + private JFrame _parentFrame; + private TextFileLoader _textFileLoader = null; + private XmlFileLoader _xmlFileLoader = null; /** @@ -95,8 +30,9 @@ public class FileLoader */ public FileLoader(App inApp, JFrame inParentFrame) { - _app = inApp; _parentFrame = inParentFrame; + _textFileLoader = new TextFileLoader(inApp, inParentFrame); + _xmlFileLoader = new XmlFileLoader(inApp, inParentFrame); } @@ -110,454 +46,40 @@ public class FileLoader _fileChooser = new JFileChooser(); if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION) { - _file = _fileChooser.getSelectedFile(); - if (preCheckFile(_file)) + File file = _fileChooser.getSelectedFile(); + // Check file exists and is readable + if (file != null && file.exists() && file.canRead()) { - _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.openoptions.title"), true); - _dialog.setLocationRelativeTo(_parentFrame); - _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); - _dialog.getContentPane().add(makeDialogComponents()); - - // select best separator according to row counts (more is better) - int bestDelim = getBestOption(_delimiterInfos[0].getNumWinningRecords(), - _delimiterInfos[1].getNumWinningRecords(), _delimiterInfos[2].getNumWinningRecords(), - _delimiterInfos[3].getNumWinningRecords()); - if (bestDelim >= 0) - _delimiterRadios[bestDelim].setSelected(true); + // Check file type to see if it's xml or just normal text + String fileExtension = file.getName().toLowerCase(); + if (fileExtension.length() > 4) + {fileExtension = fileExtension.substring(fileExtension.length() - 4);} + if (fileExtension.equals(".kml") || fileExtension.equals(".gpx") + || fileExtension.equals(".xml")) + { + // Use xml loader for kml, gpx and xml filenames + _xmlFileLoader.openFile(file); + } else - _delimiterRadios[_delimiterRadios.length-1].setSelected(true); - informDelimiterSelected(); - _dialog.pack(); - _dialog.show(); + { + // Use text loader for everything else + _textFileLoader.openFile(file); + } } else { + // couldn't read file - show error message JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.load.noread"), I18nManager.getText("error.load.dialogtitle"), JOptionPane.ERROR_MESSAGE); } } } - - /** - * Check the given file for readability and funny characters, - * and count the fields for the various separators - * @param inFile file to check - */ - private boolean preCheckFile(File inFile) - { - // Check file exists and is readable - if (inFile == null || !inFile.exists() || !inFile.canRead()) - { - return false; - } - // Use a FileCacher to read the file into an array - _fileCacher = new FileCacher(inFile); - - // Check each line of the file - String[] fileContents = _fileCacher.getContents(); - boolean fileOK = true; - _delimiterInfos = new DelimiterInfo[5]; - for (int i=0; i<4; i++) _delimiterInfos[i] = new DelimiterInfo(DELIMITERS[i]); - - String currLine = null; - String[] splitFields = null; - int commaFields = 0, semicolonFields = 0, tabFields = 0, spaceFields = 0; - for (int lineNum=0; lineNum= 0) {fileOK = false;} - // check for commas - splitFields = currLine.split(","); - commaFields = splitFields.length; - if (commaFields > 1) _delimiterInfos[0].incrementNumRecords(); - _delimiterInfos[0].updateMaxFields(commaFields); - // check for tabs - splitFields = currLine.split("\t"); - tabFields = splitFields.length; - if (tabFields > 1) _delimiterInfos[1].incrementNumRecords(); - _delimiterInfos[1].updateMaxFields(tabFields); - // check for semicolons - splitFields = currLine.split(";"); - semicolonFields = splitFields.length; - if (semicolonFields > 1) _delimiterInfos[2].incrementNumRecords(); - _delimiterInfos[2].updateMaxFields(semicolonFields); - // check for spaces - splitFields = currLine.split(" "); - spaceFields = splitFields.length; - if (spaceFields > 1) _delimiterInfos[3].incrementNumRecords(); - _delimiterInfos[3].updateMaxFields(spaceFields); - // increment counters - int bestScorer = getBestOption(commaFields, tabFields, semicolonFields, spaceFields); - if (bestScorer >= 0) - _delimiterInfos[bestScorer].incrementNumWinningRecords(); - } - return fileOK; - } - - - /** - * Get the index of the best one in the list - * @return the index of the maximum of the four given values - */ - private static int getBestOption(int inOpt0, int inOpt1, int inOpt2, int inOpt3) - { - int bestIndex = -1; - int maxScore = 1; - if (inOpt0 > maxScore) {bestIndex = 0; maxScore = inOpt0;} - if (inOpt1 > maxScore) {bestIndex = 1; maxScore = inOpt1;} - if (inOpt2 > maxScore) {bestIndex = 2; maxScore = inOpt2;} - if (inOpt3 > maxScore) {bestIndex = 3; maxScore = inOpt3;} - return bestIndex; - } - - - /** - * Make the components for the open options dialog - * @return Component for all options - */ - private Component makeDialogComponents() - { - JPanel wholePanel = new JPanel(); - wholePanel.setLayout(new BorderLayout()); - - // add buttons to south - JPanel buttonPanel = new JPanel(); - buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER)); - _backButton = new JButton(I18nManager.getText("button.back")); - _backButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) - { - _layout.previous(_cardPanel); - _backButton.setEnabled(false); - _nextButton.setEnabled(true); - _finishButton.setEnabled(false); - } - }); - _backButton.setEnabled(false); - buttonPanel.add(_backButton); - _nextButton = new JButton(I18nManager.getText("button.next")); - _nextButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) - { - prepareSecondPanel(); - _layout.next(_cardPanel); - _nextButton.setEnabled(false); - _backButton.setEnabled(true); - _finishButton.setEnabled(true); - } - }); - buttonPanel.add(_nextButton); - _finishButton = new JButton(I18nManager.getText("button.finish")); - _finishButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) - { - finished(); - } - }); - _finishButton.setEnabled(false); - buttonPanel.add(_finishButton); - JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); - cancelButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) - { - _dialog.dispose(); - } - }); - buttonPanel.add(cancelButton); - wholePanel.add(buttonPanel, BorderLayout.SOUTH); - - // Make the two cards, for delimiter and fields - _cardPanel = new JPanel(); - _layout = new CardLayout(); - _cardPanel.setLayout(_layout); - JPanel firstCard = new JPanel(); - firstCard.setLayout(new BorderLayout()); - - JPanel delimsPanel = new JPanel(); - delimsPanel.setLayout(new GridLayout(0, 2)); - delimsPanel.add(new JLabel(I18nManager.getText("dialog.delimiter.label"))); - delimsPanel.add(new JLabel("")); // blank label to go to next grid row - // radio buttons - _delimiterRadios = new JRadioButton[5]; - _delimiterRadios[0] = new JRadioButton(I18nManager.getText("dialog.delimiter.comma")); - delimsPanel.add(_delimiterRadios[0]); - _delimiterRadios[1] = new JRadioButton(I18nManager.getText("dialog.delimiter.tab")); - delimsPanel.add(_delimiterRadios[1]); - _delimiterRadios[2] = new JRadioButton(I18nManager.getText("dialog.delimiter.semicolon")); - delimsPanel.add(_delimiterRadios[2]); - _delimiterRadios[3] = new JRadioButton(I18nManager.getText("dialog.delimiter.space")); - delimsPanel.add(_delimiterRadios[3]); - JPanel otherPanel = new JPanel(); - otherPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); - _delimiterRadios[4] = new JRadioButton(I18nManager.getText("dialog.delimiter.other")); - otherPanel.add(_delimiterRadios[4]); - _otherDelimiterText = new JTextField(new OneCharDocument(), null, 2); - otherPanel.add(_otherDelimiterText); - // Group radio buttons - ButtonGroup delimGroup = new ButtonGroup(); - DelimListener delimListener = new DelimListener(); - for (int i=0; i<_delimiterRadios.length; i++) - { - delimGroup.add(_delimiterRadios[i]); - _delimiterRadios[i].addActionListener(delimListener); - } - _otherDelimiterText.getDocument().addDocumentListener(delimListener); - delimsPanel.add(new JLabel("")); - delimsPanel.add(otherPanel); - _statusLabel = new JLabel(""); - delimsPanel.add(_statusLabel); - firstCard.add(delimsPanel, BorderLayout.SOUTH); - // load snippet to show first few lines - _snippetBox = new JList(_fileCacher.getSnippet(SNIPPET_SIZE, MAX_SNIPPET_WIDTH)); - _snippetBox.setEnabled(false); - firstCard.add(makeLabelledPanel("dialog.openoptions.filesnippet", _snippetBox), BorderLayout.CENTER); - - // Second screen, for field order selection - JPanel secondCard = new JPanel(); - secondCard.setLayout(new BorderLayout()); - // table for file contents - _fileExtractTableModel = new FileExtractTableModel(); - JTable extractTable = new JTable(_fileExtractTableModel); - JScrollPane tableScrollPane = new JScrollPane(extractTable); - extractTable.setPreferredScrollableViewportSize(new Dimension(350, 80)); - extractTable.getTableHeader().setReorderingAllowed(false); - secondCard.add(makeLabelledPanel("dialog.openoptions.tabledesc", tableScrollPane), BorderLayout.NORTH); - JPanel innerPanel2 = new JPanel(); - innerPanel2.setLayout(new BorderLayout()); - _fieldTable = new JTable(new FieldSelectionTableModel()); - _fieldTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - // add listener for selected table row - _fieldTable.getSelectionModel().addListSelectionListener( - new ListSelectionListener() { - public void valueChanged(ListSelectionEvent e) { - ListSelectionModel lsm = (ListSelectionModel) e.getSource(); - if (lsm.isSelectionEmpty()) { - //no rows are selected - selectField(-1); - } else { - selectField(lsm.getMinSelectionIndex()); - } - } - }); - JPanel tablePanel = new JPanel(); - tablePanel.setLayout(new BorderLayout()); - tablePanel.add(_fieldTable.getTableHeader(), BorderLayout.NORTH); - tablePanel.add(_fieldTable, BorderLayout.CENTER); - innerPanel2.add(tablePanel, BorderLayout.CENTER); - - JPanel innerPanel3 = new JPanel(); - innerPanel3.setLayout(new BoxLayout(innerPanel3, BoxLayout.Y_AXIS)); - _moveUpButton = new JButton(I18nManager.getText("button.moveup")); - _moveUpButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) - { - int currRow = _fieldTable.getSelectedRow(); - closeTableComboBox(currRow); - _fieldTableModel.moveUp(currRow); - _fieldTable.setRowSelectionInterval(currRow-1, currRow-1); - } - }); - innerPanel3.add(_moveUpButton); - _moveDownButton = new JButton(I18nManager.getText("button.movedown")); - _moveDownButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) - { - int currRow = _fieldTable.getSelectedRow(); - closeTableComboBox(currRow); - _fieldTableModel.moveDown(currRow); - _fieldTable.setRowSelectionInterval(currRow+1, currRow+1); - } - }); - innerPanel3.add(_moveDownButton); - innerPanel3.add(Box.createVerticalStrut(70)); - - innerPanel2.add(innerPanel3, BorderLayout.EAST); - secondCard.add(innerPanel2, BorderLayout.CENTER); - JPanel altUnitsPanel = new JPanel(); - altUnitsPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); - altUnitsPanel.add(new JLabel(I18nManager.getText("dialog.openoptions.altitudeunits"))); - String[] units = {I18nManager.getText("units.metres"), I18nManager.getText("units.feet")}; - _unitsDropDown = new JComboBox(units); - altUnitsPanel.add(_unitsDropDown); - secondCard.add(altUnitsPanel, BorderLayout.SOUTH); - _cardPanel.add(firstCard, "card1"); - _cardPanel.add(secondCard, "card2"); - - wholePanel.add(_cardPanel, BorderLayout.CENTER); - return wholePanel; - } - - - /** - * Close the combo box on the selected row of the field table - * @param inRow currently selected row number - */ - private void closeTableComboBox(int inRow) - { - TableCellEditor editor = _fieldTable.getCellEditor(inRow, 1); - if (editor != null) - { - editor.stopCellEditing(); - } - } - - - /** - * change the status based on selection of a delimiter - */ - protected void informDelimiterSelected() - { - for (int i=0; i<(_delimiterRadios.length-1); i++) - { - if (_delimiterRadios[i].isSelected()) - { - int numRecords = _delimiterInfos[i].getNumRecords(); - if (numRecords == 0) - { - _statusLabel.setText(I18nManager.getText("dialog.openoptions.deliminfo.norecords")); - } - else - { - _statusLabel.setText("" + numRecords + " " + I18nManager.getText("dialog.openoptions.deliminfo.records") - + _delimiterInfos[i].getMaxFields() + " " + I18nManager.getText("dialog.openoptions.deliminfo.fields")); - } - } - } - if (_delimiterRadios[_delimiterRadios.length-1].isSelected()) - { - _statusLabel.setText(""); - } - // enable/disable next button - _nextButton.setEnabled(_delimiterRadios[4].isSelected() == false - || _otherDelimiterText.getText().length() == 1); - } - - - /** - * Get the delimiter info from the first step - * @return delimiter information object for the selected delimiter - */ - public DelimiterInfo getSelectedDelimiterInfo() - { - for (int i=0; i<4; i++) - if (_delimiterRadios[i].isSelected()) return _delimiterInfos[i]; - // must be "other" - build info if necessary - if (_delimiterInfos[4] == null) - _delimiterInfos[4] = new DelimiterInfo(_otherDelimiterText.getText().charAt(0)); - return _delimiterInfos[4]; - } - - - /** - * Use the delimiter selected to determine the fields in the file - * and prepare the second panel accordingly - */ - private void prepareSecondPanel() - { - DelimiterInfo info = getSelectedDelimiterInfo(); - 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()); - // possible to ignore blank columns here - _currentDelimiter = info.getDelimiter(); - _fileExtractTableModel.updateData(tableData); - _fieldTableModel = new FieldSelectionTableModel(); - - // 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(); - _fieldTableModel.updateData(startFieldArray); - _fieldTable.setModel(_fieldTableModel); - // add dropdowns to second column - JComboBox fieldTypesBox = new JComboBox(); - for (int i=0; i 0); - _moveDownButton.setEnabled(inFieldNum >= 0 - && inFieldNum < (_fieldTableModel.getRowCount()-1)); - } - } - - /** - * @return the last delimiter character used for a load + * @return the last delimiter character used for a text file load */ public char getLastUsedDelimiter() { - return _lastUsedDelimiter; + return _textFileLoader.getLastUsedDelimiter(); } } diff --git a/tim/prune/load/JpegLoader.java b/tim/prune/load/JpegLoader.java index 6fe52a1..e6917c1 100644 --- a/tim/prune/load/JpegLoader.java +++ b/tim/prune/load/JpegLoader.java @@ -24,6 +24,8 @@ import tim.prune.data.DataPoint; import tim.prune.data.Latitude; import tim.prune.data.Longitude; import tim.prune.data.Photo; +import tim.prune.data.PhotoStatus; +import tim.prune.data.Timestamp; import tim.prune.drew.jpeg.ExifReader; import tim.prune.drew.jpeg.JpegData; import tim.prune.drew.jpeg.JpegException; @@ -38,6 +40,7 @@ public class JpegLoader implements Runnable private JFrame _parentFrame = null; private JFileChooser _fileChooser = null; private JCheckBox _subdirCheckbox = null; + private JCheckBox _noExifCheckbox = null; private JDialog _progressDialog = null; private JProgressBar _progressBar = null; private int[] _fileCounts = null; @@ -56,6 +59,7 @@ public class JpegLoader implements Runnable _parentFrame = inParentFrame; } + /** * Select an input file and open the GUI frame * to select load options @@ -67,9 +71,16 @@ public class JpegLoader implements Runnable _fileChooser = new JFileChooser(); _fileChooser.setMultiSelectionEnabled(true); _fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + _fileChooser.setDialogTitle(I18nManager.getText("menu.file.addphotos")); _subdirCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.subdirectories")); _subdirCheckbox.setSelected(true); - _fileChooser.setAccessory(_subdirCheckbox); + _noExifCheckbox = new JCheckBox(I18nManager.getText("dialog.jpegload.loadjpegswithoutcoords")); + _noExifCheckbox.setSelected(true); + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.add(_subdirCheckbox); + panel.add(_noExifCheckbox); + _fileChooser.setAccessory(panel); } if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION) { @@ -118,40 +129,50 @@ public class JpegLoader implements Runnable // Initialise arrays, errors, summaries _fileCounts = new int[4]; // files, jpegs, exifs, gps _photos = new ArrayList(); - // Loop over selected files/directories File[] files = _fileChooser.getSelectedFiles(); + // Loop recursively over selected files/directories to count files int numFiles = countFileList(files, true, _subdirCheckbox.isSelected()); - // if (false) System.out.println("Found " + numFiles + " files"); + // Set up the progress bar for this number of files _progressBar.setMaximum(numFiles); _progressBar.setValue(0); _cancelled = false; + + // Process the files recursively and build lists of photos processFileList(files, true, _subdirCheckbox.isSelected()); _progressDialog.hide(); - if (_cancelled) return; - // System.out.println("Finished - counts are: " + _fileCounts[0] + ", " + _fileCounts[1] + ", " + _fileCounts[2] + ", " + _fileCounts[3]); + if (_cancelled) {return;} + + //System.out.println("Finished - counts are: " + _fileCounts[0] + ", " + _fileCounts[1] + // + ", " + _fileCounts[2] + ", " + _fileCounts[3]); if (_fileCounts[0] == 0) { + // No files found at all JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nofilesfound"), I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE); } else if (_fileCounts[1] == 0) { + // No jpegs found JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nojpegsfound"), I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE); } - else if (_fileCounts[2] == 0) + else if (!_noExifCheckbox.isSelected() && _fileCounts[2] == 0) { + // Need coordinates but no exif found JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.noexiffound"), I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE); } - else if (_fileCounts[3] == 0) + else if (!_noExifCheckbox.isSelected() && _fileCounts[3] == 0) { + // Need coordinates but no gps information found JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.jpegload.nogpsfound"), I18nManager.getText("error.jpegload.dialogtitle"), JOptionPane.ERROR_MESSAGE); } else { - // Load information into dialog for confirmation + // Found some photos to load + // TODO: Load jpeg information into dialog for confirmation? + // Pass information back to app _app.informPhotosLoaded(_photos); } } @@ -182,14 +203,15 @@ public class JpegLoader implements Runnable { // Always process first directory, // only process subdirectories if checkbox selected - processDirectory(file, inDescend); + File[] files = file.listFiles(); + processFileList(files, false, inDescend); } } else { - // file doesn't exist or isn't readable - record error + // file doesn't exist or isn't readable - ignore error } - // check for cancel + // check for cancel button pressed if (_cancelled) break; } } @@ -202,52 +224,55 @@ public class JpegLoader implements Runnable */ private void processFile(File inFile) { + // Update progress bar _fileCounts[0]++; // file found _progressBar.setValue(_fileCounts[0]); _progressBar.setString("" + _fileCounts[0] + " / " + _progressBar.getMaximum()); _progressBar.repaint(); + + // Check whether filename corresponds with accepted filenames + if (!acceptPhotoFilename(inFile.getName())) {return;} + + // Create Photo object + Photo photo = new Photo(inFile); + // Try to get information out of exif try { JpegData jpegData = new ExifReader(inFile).extract(); _fileCounts[1]++; // jpeg found (no exception thrown) -// if (jpegData.getNumErrors() > 0) -// System.out.println("Number of errors was: " + jpegData.getNumErrors() + ": " + jpegData.getErrors().get(0)); if (jpegData.getExifDataPresent()) - _fileCounts[2]++; // exif found + {_fileCounts[2]++;} // exif found if (jpegData.isValid()) { -// if (false && jpegData.getTimestamp() != null) -// System.out.println("Timestamp is " + jpegData.getTimestamp()[0].toString() + ":" + jpegData.getTimestamp()[1].toString() + ":" + jpegData.getTimestamp()[2].toString()); -// if (false && jpegData.getDatestamp() != null) -// System.out.println("Datestamp is " + jpegData.getDatestamp()[0].toString() + ":" + jpegData.getDatestamp()[1].toString() + ":" + jpegData.getDatestamp()[2].toString()); - // Make DataPoint and Photo + if (jpegData.getDatestamp() != null && jpegData.getTimestamp() != null) + { + photo.setTimestamp(createTimestamp(jpegData.getDatestamp(), jpegData.getTimestamp())); + } + // Make DataPoint and attach to Photo DataPoint point = createDataPoint(jpegData); - Photo photo = new Photo(inFile); point.setPhoto(photo); photo.setDataPoint(point); - _photos.add(photo); -// System.out.println("Made photo: " + photo.getFile().getAbsolutePath() + " with the datapoint: " -// + point.getLatitude().output(Latitude.FORMAT_DEG_MIN_SEC) + ", " -// + point.getLongitude().output(Longitude.FORMAT_DEG_MIN_SEC) + ", " -// + point.getAltitude().getValue(Altitude.FORMAT_METRES)); + photo.setOriginalStatus(PhotoStatus.TAGGED); _fileCounts[3]++; } } catch (JpegException jpe) { // don't list errors, just count them } - } - - - /** - * Process the given directory, by looping over its contents - * and recursively through its subdirectories - * @param inDirectory directory to read - * @param inDescend true to descend subdirectories - */ - private void processDirectory(File inDirectory, boolean inDescend) - { - File[] files = inDirectory.listFiles(); - processFileList(files, false, inDescend); + // Use file timestamp if exif timestamp isn't available + if (photo.getTimestamp() == null) + { + photo.setTimestamp(new Timestamp(inFile.lastModified())); + //System.out.println("No exif, using timestamp from file: " + inFile.lastModified() + " -> " + photo.getTimestamp().getText()); + } + else + { + //System.out.println("timestamp from file = " + photo.getTimestamp().getText()); + } + // Add the photo if it's got a point or if pointless photos should be added + if (photo.getDataPoint() != null || _noExifCheckbox.isSelected()) + { + _photos.add(photo); + } } @@ -295,11 +320,15 @@ public class JpegLoader implements Runnable // Create model objects from jpeg data double latval = getCoordinateDoubleValue(inData.getLatitude(), inData.getLatitudeRef() == 'N' || inData.getLatitudeRef() == 'n'); - Latitude latitude = new Latitude(latval, Latitude.FORMAT_NONE); + Latitude latitude = new Latitude(latval, Latitude.FORMAT_DEG_MIN_SEC); double lonval = getCoordinateDoubleValue(inData.getLongitude(), inData.getLongitudeRef() == 'E' || inData.getLongitudeRef() == 'e'); - Longitude longitude = new Longitude(lonval, Longitude.FORMAT_NONE); - Altitude altitude = new Altitude(inData.getAltitude().intValue(), Altitude.FORMAT_METRES); + Longitude longitude = new Longitude(lonval, Longitude.FORMAT_DEG_MIN_SEC); + Altitude altitude = null; + if (inData.getAltitude() != null) + { + altitude = new Altitude(inData.getAltitude().intValue(), Altitude.FORMAT_METRES); + } return new DataPoint(latitude, longitude, altitude); } @@ -321,4 +350,49 @@ public class JpegLoader implements Runnable if (!isPositive) value = -value; return value; } + + + /** + * Use the given Rational values to create a timestamp + * @param inDate rationals describing date + * @param inTime rationals describing time + * @return Timestamp object corresponding to inputs + */ + private static Timestamp createTimestamp(Rational[] inDate, Rational[] inTime) + { + //System.out.println("Making timestamp for date (" + inDate[0].toString() + "," + inDate[1].toString() + "," + inDate[2].toString() + ") and time (" + // + inTime[0].toString() + "," + inTime[1].toString() + "," + inTime[2].toString() + ")"); + return new Timestamp(inDate[0].intValue(), inDate[1].intValue(), inDate[2].intValue(), + inTime[0].intValue(), inTime[1].intValue(), inTime[2].intValue()); + } + + + /** + * Check whether to accept the given filename + * @param inName name of file + * @return true if accepted, false otherwise + */ + private static boolean acceptPhotoFilename(String inName) + { + if (inName != null && inName.length() > 4) + { + // Check for three-character file extensions jpg and jpe + String lastFour = inName.substring(inName.length() - 4).toLowerCase(); + if (lastFour.equals(".jpg") || lastFour.equals(".jpe")) + { + return true; + } + // If not found, check for file extension jpeg + if (inName.length() > 5) + { + String lastFive = inName.substring(inName.length() - 5).toLowerCase(); + if (lastFive.equals(".jpeg")) + { + return true; + } + } + } + // Not matched so don't accept + return false; + } } diff --git a/tim/prune/load/PhotoMeasurer.java b/tim/prune/load/PhotoMeasurer.java new file mode 100644 index 0000000..710508a --- /dev/null +++ b/tim/prune/load/PhotoMeasurer.java @@ -0,0 +1,60 @@ +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 + } +} diff --git a/tim/prune/load/TextFileLoader.java b/tim/prune/load/TextFileLoader.java new file mode 100644 index 0000000..b692631 --- /dev/null +++ b/tim/prune/load/TextFileLoader.java @@ -0,0 +1,561 @@ +package tim.prune.load; + +import java.awt.BorderLayout; +import java.awt.CardLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.TableCellEditor; + +import java.io.File; + +import tim.prune.App; +import tim.prune.I18nManager; +import tim.prune.data.Altitude; +import tim.prune.data.Field; + + +/** + * Class to handle loading of text files including GUI options, + * and passing loaded data back to App object + */ +public class TextFileLoader +{ + private File _file = null; + private App _app = null; + private JFrame _parentFrame = null; + private JDialog _dialog = null; + private JPanel _cardPanel = null; + private CardLayout _layout = null; + private JButton _backButton = null, _nextButton = null; + private JButton _finishButton = null; + private JButton _moveUpButton = null, _moveDownButton = null; + private JRadioButton[] _delimiterRadios = null; + private JTextField _otherDelimiterText = null; + private JLabel _statusLabel = null; + private DelimiterInfo[] _delimiterInfos = null; + private FileCacher _fileCacher = null; + private JList _snippetBox = null; + private FileExtractTableModel _fileExtractTableModel = null; + private JTable _fieldTable; + private FieldSelectionTableModel _fieldTableModel = null; + private JComboBox _unitsDropDown = null; + private int _selectedField = -1; + private char _currentDelimiter = ','; + + // previously selected values + private char _lastUsedDelimiter = ','; + private Field[] _lastSelectedFields = null; + private int _lastAltitudeFormat = Altitude.FORMAT_NONE; + + // constants + private static final int SNIPPET_SIZE = 6; + private static final int MAX_SNIPPET_WIDTH = 80; + private static final char[] DELIMITERS = {',', '\t', ';', ' '}; + + + /** + * Inner class to listen for delimiter change operations + */ + private class DelimListener implements ActionListener, DocumentListener + { + public void actionPerformed(ActionEvent e) + { + informDelimiterSelected(); + } + public void changedUpdate(DocumentEvent e) + { + informDelimiterSelected(); + } + public void insertUpdate(DocumentEvent e) + { + informDelimiterSelected(); + } + public void removeUpdate(DocumentEvent e) + { + informDelimiterSelected(); + } + } + + + /** + * Constructor + * @param inApp Application object to inform of track load + * @param inParentFrame parent frame to reference for dialogs + */ + public TextFileLoader(App inApp, JFrame inParentFrame) + { + _app = inApp; + _parentFrame = inParentFrame; + } + + + /** + * Open the selected file and show the GUI dialog + * to select load options + */ + public void openFile(File inFile) + { + _file = inFile; + if (preCheckFile(_file)) + { + _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.openoptions.title"), true); + _dialog.setLocationRelativeTo(_parentFrame); + _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + _dialog.getContentPane().add(makeDialogComponents()); + + // select best separator according to row counts (more is better) + int bestDelim = getBestOption(_delimiterInfos[0].getNumWinningRecords(), + _delimiterInfos[1].getNumWinningRecords(), _delimiterInfos[2].getNumWinningRecords(), + _delimiterInfos[3].getNumWinningRecords()); + if (bestDelim >= 0) + _delimiterRadios[bestDelim].setSelected(true); + else + _delimiterRadios[_delimiterRadios.length-1].setSelected(true); + informDelimiterSelected(); + _dialog.pack(); + _dialog.show(); + } + else + { + JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.load.noread"), + I18nManager.getText("error.load.dialogtitle"), JOptionPane.ERROR_MESSAGE); + } + } + + + /** + * Check the given file for readability and funny characters, + * and count the fields for the various separators + * @param inFile file to check + */ + private boolean preCheckFile(File inFile) + { + // Check file exists and is readable + if (inFile == null || !inFile.exists() || !inFile.canRead()) + { + return false; + } + // Use a FileCacher to read the file into an array + _fileCacher = new FileCacher(inFile); + + // Check each line of the file + String[] fileContents = _fileCacher.getContents(); + boolean fileOK = true; + _delimiterInfos = new DelimiterInfo[5]; + for (int i=0; i<4; i++) _delimiterInfos[i] = new DelimiterInfo(DELIMITERS[i]); + + String currLine = null; + String[] splitFields = null; + int commaFields = 0, semicolonFields = 0, tabFields = 0, spaceFields = 0; + for (int lineNum=0; lineNum= 0) {fileOK = false;} + // check for commas + splitFields = currLine.split(","); + commaFields = splitFields.length; + if (commaFields > 1) _delimiterInfos[0].incrementNumRecords(); + _delimiterInfos[0].updateMaxFields(commaFields); + // check for tabs + splitFields = currLine.split("\t"); + tabFields = splitFields.length; + if (tabFields > 1) _delimiterInfos[1].incrementNumRecords(); + _delimiterInfos[1].updateMaxFields(tabFields); + // check for semicolons + splitFields = currLine.split(";"); + semicolonFields = splitFields.length; + if (semicolonFields > 1) _delimiterInfos[2].incrementNumRecords(); + _delimiterInfos[2].updateMaxFields(semicolonFields); + // check for spaces + splitFields = currLine.split(" "); + spaceFields = splitFields.length; + if (spaceFields > 1) _delimiterInfos[3].incrementNumRecords(); + _delimiterInfos[3].updateMaxFields(spaceFields); + // increment counters + int bestScorer = getBestOption(commaFields, tabFields, semicolonFields, spaceFields); + if (bestScorer >= 0) + _delimiterInfos[bestScorer].incrementNumWinningRecords(); + } + return fileOK; + } + + + /** + * Get the index of the best one in the list + * @return the index of the maximum of the four given values + */ + private static int getBestOption(int inOpt0, int inOpt1, int inOpt2, int inOpt3) + { + int bestIndex = -1; + int maxScore = 1; + if (inOpt0 > maxScore) {bestIndex = 0; maxScore = inOpt0;} + if (inOpt1 > maxScore) {bestIndex = 1; maxScore = inOpt1;} + if (inOpt2 > maxScore) {bestIndex = 2; maxScore = inOpt2;} + if (inOpt3 > maxScore) {bestIndex = 3; maxScore = inOpt3;} + return bestIndex; + } + + + /** + * Make the components for the open options dialog + * @return Component for all options + */ + private Component makeDialogComponents() + { + JPanel wholePanel = new JPanel(); + wholePanel.setLayout(new BorderLayout()); + + // add buttons to south + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER)); + _backButton = new JButton(I18nManager.getText("button.back")); + _backButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _layout.previous(_cardPanel); + _backButton.setEnabled(false); + _nextButton.setEnabled(true); + _finishButton.setEnabled(false); + } + }); + _backButton.setEnabled(false); + buttonPanel.add(_backButton); + _nextButton = new JButton(I18nManager.getText("button.next")); + _nextButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + prepareSecondPanel(); + _layout.next(_cardPanel); + _nextButton.setEnabled(false); + _backButton.setEnabled(true); + _finishButton.setEnabled(_fieldTableModel.getRowCount() > 1); + } + }); + buttonPanel.add(_nextButton); + _finishButton = new JButton(I18nManager.getText("button.finish")); + _finishButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + finished(); + } + }); + _finishButton.setEnabled(false); + buttonPanel.add(_finishButton); + JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _dialog.dispose(); + } + }); + buttonPanel.add(cancelButton); + wholePanel.add(buttonPanel, BorderLayout.SOUTH); + + // Make the two cards, for delimiter and fields + _cardPanel = new JPanel(); + _layout = new CardLayout(); + _cardPanel.setLayout(_layout); + JPanel firstCard = new JPanel(); + firstCard.setLayout(new BorderLayout()); + + JPanel delimsPanel = new JPanel(); + delimsPanel.setLayout(new GridLayout(0, 2)); + delimsPanel.add(new JLabel(I18nManager.getText("dialog.delimiter.label"))); + delimsPanel.add(new JLabel("")); // blank label to go to next grid row + // radio buttons + _delimiterRadios = new JRadioButton[5]; + _delimiterRadios[0] = new JRadioButton(I18nManager.getText("dialog.delimiter.comma")); + delimsPanel.add(_delimiterRadios[0]); + _delimiterRadios[1] = new JRadioButton(I18nManager.getText("dialog.delimiter.tab")); + delimsPanel.add(_delimiterRadios[1]); + _delimiterRadios[2] = new JRadioButton(I18nManager.getText("dialog.delimiter.semicolon")); + delimsPanel.add(_delimiterRadios[2]); + _delimiterRadios[3] = new JRadioButton(I18nManager.getText("dialog.delimiter.space")); + delimsPanel.add(_delimiterRadios[3]); + JPanel otherPanel = new JPanel(); + otherPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0)); + _delimiterRadios[4] = new JRadioButton(I18nManager.getText("dialog.delimiter.other")); + otherPanel.add(_delimiterRadios[4]); + _otherDelimiterText = new JTextField(new OneCharDocument(), null, 2); + otherPanel.add(_otherDelimiterText); + // Group radio buttons + ButtonGroup delimGroup = new ButtonGroup(); + DelimListener delimListener = new DelimListener(); + for (int i=0; i<_delimiterRadios.length; i++) + { + delimGroup.add(_delimiterRadios[i]); + _delimiterRadios[i].addActionListener(delimListener); + } + _otherDelimiterText.getDocument().addDocumentListener(delimListener); + delimsPanel.add(new JLabel("")); + delimsPanel.add(otherPanel); + _statusLabel = new JLabel(""); + delimsPanel.add(_statusLabel); + firstCard.add(delimsPanel, BorderLayout.SOUTH); + // load snippet to show first few lines + _snippetBox = new JList(_fileCacher.getSnippet(SNIPPET_SIZE, MAX_SNIPPET_WIDTH)); + _snippetBox.setEnabled(false); + firstCard.add(makeLabelledPanel("dialog.openoptions.filesnippet", _snippetBox), BorderLayout.CENTER); + + // Second screen, for field order selection + JPanel secondCard = new JPanel(); + secondCard.setLayout(new BorderLayout()); + // table for file contents + _fileExtractTableModel = new FileExtractTableModel(); + JTable extractTable = new JTable(_fileExtractTableModel); + JScrollPane tableScrollPane = new JScrollPane(extractTable); + extractTable.setPreferredScrollableViewportSize(new Dimension(350, 80)); + extractTable.getTableHeader().setReorderingAllowed(false); + secondCard.add(makeLabelledPanel("dialog.openoptions.tabledesc", tableScrollPane), BorderLayout.NORTH); + JPanel innerPanel2 = new JPanel(); + innerPanel2.setLayout(new BorderLayout()); + _fieldTable = new JTable(new FieldSelectionTableModel()); + _fieldTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + // add listener for selected table row + _fieldTable.getSelectionModel().addListSelectionListener( + new ListSelectionListener() { + public void valueChanged(ListSelectionEvent e) { + ListSelectionModel lsm = (ListSelectionModel) e.getSource(); + if (lsm.isSelectionEmpty()) { + //no rows are selected + selectField(-1); + } else { + selectField(lsm.getMinSelectionIndex()); + } + } + }); + JPanel tablePanel = new JPanel(); + tablePanel.setLayout(new BorderLayout()); + tablePanel.add(_fieldTable.getTableHeader(), BorderLayout.NORTH); + tablePanel.add(_fieldTable, BorderLayout.CENTER); + innerPanel2.add(tablePanel, BorderLayout.CENTER); + + JPanel innerPanel3 = new JPanel(); + innerPanel3.setLayout(new BoxLayout(innerPanel3, BoxLayout.Y_AXIS)); + _moveUpButton = new JButton(I18nManager.getText("button.moveup")); + _moveUpButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + int currRow = _fieldTable.getSelectedRow(); + closeTableComboBox(currRow); + _fieldTableModel.moveUp(currRow); + _fieldTable.setRowSelectionInterval(currRow-1, currRow-1); + } + }); + innerPanel3.add(_moveUpButton); + _moveDownButton = new JButton(I18nManager.getText("button.movedown")); + _moveDownButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + int currRow = _fieldTable.getSelectedRow(); + closeTableComboBox(currRow); + _fieldTableModel.moveDown(currRow); + _fieldTable.setRowSelectionInterval(currRow+1, currRow+1); + } + }); + innerPanel3.add(_moveDownButton); + innerPanel3.add(Box.createVerticalStrut(70)); + + innerPanel2.add(innerPanel3, BorderLayout.EAST); + secondCard.add(innerPanel2, BorderLayout.CENTER); + JPanel altUnitsPanel = new JPanel(); + altUnitsPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); + altUnitsPanel.add(new JLabel(I18nManager.getText("dialog.openoptions.altitudeunits"))); + String[] units = {I18nManager.getText("units.metres"), I18nManager.getText("units.feet")}; + _unitsDropDown = new JComboBox(units); + altUnitsPanel.add(_unitsDropDown); + secondCard.add(altUnitsPanel, BorderLayout.SOUTH); + _cardPanel.add(firstCard, "card1"); + _cardPanel.add(secondCard, "card2"); + + wholePanel.add(_cardPanel, BorderLayout.CENTER); + return wholePanel; + } + + + /** + * Close the combo box on the selected row of the field table + * @param inRow currently selected row number + */ + private void closeTableComboBox(int inRow) + { + TableCellEditor editor = _fieldTable.getCellEditor(inRow, 1); + if (editor != null) + { + editor.stopCellEditing(); + } + } + + + /** + * change the status based on selection of a delimiter + */ + protected void informDelimiterSelected() + { + int fields = 0; + // Loop through radios to see which one is selected + for (int i=0; i<(_delimiterRadios.length-1); i++) + { + if (_delimiterRadios[i].isSelected()) + { + // Set label text to describe records and fields + int numRecords = _delimiterInfos[i].getNumRecords(); + if (numRecords == 0) + { + _statusLabel.setText(I18nManager.getText("dialog.openoptions.deliminfo.norecords")); + } + else + { + fields = _delimiterInfos[i].getMaxFields(); + _statusLabel.setText("" + numRecords + " " + I18nManager.getText("dialog.openoptions.deliminfo.records") + + fields + " " + I18nManager.getText("dialog.openoptions.deliminfo.fields")); + } + } + } + // Don't show label if "other" delimiter is chosen (as records, fields are unknown) + if (_delimiterRadios[_delimiterRadios.length-1].isSelected()) + { + _statusLabel.setText(""); + } + // enable/disable next button + _nextButton.setEnabled((_delimiterRadios[4].isSelected() == false && fields > 1) + || _otherDelimiterText.getText().length() == 1); + } + + + /** + * Get the delimiter info from the first step + * @return delimiter information object for the selected delimiter + */ + public DelimiterInfo getSelectedDelimiterInfo() + { + for (int i=0; i<4; i++) + if (_delimiterRadios[i].isSelected()) return _delimiterInfos[i]; + // must be "other" - build info if necessary + if (_delimiterInfos[4] == null) + _delimiterInfos[4] = new DelimiterInfo(_otherDelimiterText.getText().charAt(0)); + return _delimiterInfos[4]; + } + + + /** + * Use the delimiter selected to determine the fields in the file + * and prepare the second panel accordingly + */ + private void prepareSecondPanel() + { + DelimiterInfo info = getSelectedDelimiterInfo(); + 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()); + // possible to ignore blank columns here + _currentDelimiter = info.getDelimiter(); + _fileExtractTableModel.updateData(tableData); + _fieldTableModel = new FieldSelectionTableModel(); + + // 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(); + _fieldTableModel.updateData(startFieldArray); + _fieldTable.setModel(_fieldTableModel); + // add dropdowns to second column + JComboBox fieldTypesBox = new JComboBox(); + for (int i=0; i 0); + _moveDownButton.setEnabled(inFieldNum >= 0 + && inFieldNum < (_fieldTableModel.getRowCount()-1)); + } + } + + + /** + * @return the last delimiter character used for a load + */ + public char getLastUsedDelimiter() + { + return _lastUsedDelimiter; + } +} diff --git a/tim/prune/load/xml/GpxHandler.java b/tim/prune/load/xml/GpxHandler.java new file mode 100644 index 0000000..962b495 --- /dev/null +++ b/tim/prune/load/xml/GpxHandler.java @@ -0,0 +1,157 @@ +package tim.prune.load.xml; + +import java.util.ArrayList; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +import tim.prune.data.Field; + + +/** + * Class for handling specifics of parsing Gpx files + */ +public class GpxHandler extends XmlHandler +{ + private boolean _insideName = false; + private boolean _insideElevation = false; + private boolean _insideTime = false; + private String _name = null, _latitude = null, _longitude = null; + private String _elevation = null; + private String _time = null; + private ArrayList _pointList = new ArrayList(); + + + /** + * Receive the start of a tag + * @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 + { + // Read the parameters for waypoints and track points + if (qName.equalsIgnoreCase("wpt") || qName.equalsIgnoreCase("trkpt")) + { + int numAttributes = attributes.getLength(); + for (int i=0; i 1) + { + // Add each of the unnamed track points to list + for (int p=0; p + + // To set altitude with overwrite: exiftool -P -overwrite_original_in_place -GPSAltitude=1234 -GPSAltitudeRef='Above Sea Level' + // (setting altitude ref to 0 doesn't work) + // To set latitude with overwrite: exiftool -P -overwrite_original_in_place -GPSLatitude='12 34 56.78' -GPSLatitudeRef=N + // (latitude as space-separated deg min sec, reference as either N or S) + // Same for longitude, reference E or W + + + /** + * Constructor + * @param inParentFrame parent frame + */ + public ExifSaver(Frame inParentFrame) + { + _parentFrame = inParentFrame; + } + + + /** + * Save exif information to all photos in the list + * whose coordinate information has changed since loading + * @param inPhotoList list of photos to save + */ + public boolean saveExifInformation(PhotoList inPhotoList) + { + // Check if external exif tool can be called + boolean exifToolInstalled = ExternalTools.isExiftoolInstalled(); + if (!exifToolInstalled) + { + // show warning + int answer = JOptionPane.showConfirmDialog(_dialog, I18nManager.getText("dialog.saveexif.noexiftool"), + I18nManager.getText("dialog.saveexif.title"), + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + if (answer == JOptionPane.NO_OPTION || answer == JOptionPane.CLOSED_OPTION) + { + return false; + } + } + // Make model and add all photos to it + _photoTableModel = new PhotoTableModel(inPhotoList.getNumPhotos()); + for (int i=0; i 0; + _exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected()); + _exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected()); + } + + + /** + * 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(".kml") || f.getName().toLowerCase().endsWith(".kmz"))); + } + public String getDescription() + { + return I18nManager.getText("dialog.exportkml.filetype"); + } + }); + String requiredExtension = null, otherExtension = null; + if (_kmzCheckbox.isSelected()) { - writer = new FileWriter(inFile); - writer.write("\n\n\n"); - writer.write("\t"); - writer.write(inDescription); - writer.write("\n"); - - int i = 0; - DataPoint point = null; - boolean hasTrackpoints = false; - // Loop over waypoints - boolean writtenPhotoHeader = false; - int numPoints = _track.getNumPoints(); - for (i=0; ihttp://maps.google.com/mapfiles/kml/pal4/icon46.png"); - writtenPhotoHeader = true; - } - exportPhotoPoint(point, writer); + // New file or overwrite confirmed, so initiate export in separate thread + _exportFile = file; + new Thread(this).start(); } else { - hasTrackpoints = true; + chooseAgain = true; } } - if (hasTrackpoints) + } while (chooseAgain); + } + + + /** + * Run method for controlling separate thread for exporting + */ + public void run() + { + // Initialise progress bar + _progressBar.setVisible(true); + _progressBar.setValue(0); + boolean exportToKmz = _kmzCheckbox.isSelected(); + boolean exportImages = exportToKmz && _exportImagesCheckbox.isSelected(); + _progressBar.setMaximum(exportImages?getNumPhotosToExport():1); + OutputStreamWriter writer = null; + ZipOutputStream zipOutputStream = null; + try + { + // Select writer according to whether kmz requested or not + if (!_kmzCheckbox.isSelected()) + { + // normal writing to file + writer = new OutputStreamWriter(new FileOutputStream(_exportFile)); + } + else { - writer.write("\t\n\t\ttrack\n\t\t\n\t\t\n\t\t\t"); - // Loop over track points - for (i=0; i.jpg + exportThumbnails(zipOutputStream); } - writer.write("\t\t\t\n\t\t\n\t"); } - writer.write("\n"); + + // close file writer.close(); JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.ok1") + " " + numPoints + " " + I18nManager.getText("dialog.save.ok2") - + " " + inFile.getAbsolutePath(), + + " " + _exportFile.getAbsolutePath(), I18nManager.getText("dialog.save.oktitle"), JOptionPane.INFORMATION_MESSAGE); - return true; + // 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(), + JOptionPane.showMessageDialog(_parentFrame, + I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(), I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE); } - return false; + // 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 + * @param inExportImages true if image thumbnails are to be referenced + * @return number of points written + */ + private int exportData(OutputStreamWriter inWriter, boolean inExportImages) + throws IOException + { + // TODO: Look at segments of track, and split into separate lines in Kml if necessary + inWriter.write("\n\n\n"); + inWriter.write("\t"); + if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals("")) + { + inWriter.write(_descriptionField.getText()); + } + else + { + inWriter.write("Export from Prune"); + } + inWriter.write("\n"); + + int i = 0; + DataPoint point = null; + boolean hasTrackpoints = false; + // Loop over waypoints + boolean writtenPhotoHeader = false; + int numPoints = _track.getNumPoints(); + int photoNum = 0; + for (i=0; ihttp://maps.google.com/mapfiles/kml/pal4/icon46.png"); + writtenPhotoHeader = true; + } + photoNum++; + exportPhotoPoint(point, inWriter, inExportImages, photoNum); + } + else + { + hasTrackpoints = true; + } + } + // Make a line for the track, if there is one + if (hasTrackpoints) + { + inWriter.write("\t\n\t\ttrack\n\t\t\n\t\t\n\t\t\t"); + // Loop over track points + for (i=0; i\n\t\t\n\t"); + } + inWriter.write("\n"); + return numPoints; } @@ -214,14 +415,26 @@ public class KmlExporter * Export the specified photo into the file * @param inPoint data point including photo * @param inWriter writer object + * @param inImageLink flag to set whether to export image links or not + * @param inImageNumber number of image for filename * @throws IOException on write failure */ - private void exportPhotoPoint(DataPoint inPoint, Writer inWriter) throws IOException + private void exportPhotoPoint(DataPoint inPoint, Writer inWriter, boolean inImageLink, int inImageNumber) + throws IOException { - // TODO: Export photos to KML too - for photos need kmz! inWriter.write("\t\n\t\t"); inWriter.write(inPoint.getPhoto().getFile().getName()); inWriter.write("\n"); + if (inImageLink) + { + // Work out image dimensions of thumbnail + Dimension picSize = inPoint.getPhoto().getSize(); + Dimension thumbSize = ImageUtils.getThumbnailSize(picSize.width, picSize.height, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT); + // Write out some html for the thumbnail images + inWriter.write("" + + "
Caption for the photo
]]>
"); + } inWriter.write("#camera_icon\n"); inWriter.write("\t\t\n\t\t\t"); inWriter.write(inPoint.getLongitude().output(Coordinate.FORMAT_DEG_WITHOUT_CARDINAL)); @@ -244,4 +457,72 @@ public class KmlExporter // Altitude not exported, locked to ground by Google Earth inWriter.write(",0\n"); } + + + /** + * Loop through the photos and create thumbnails + * @param inZipStream zip stream to save image files to + */ + private void exportThumbnails(ZipOutputStream inZipStream) throws IOException + { + // set up image writer + Iterator writers = ImageIO.getImageWritersByFormatName("jpg"); + if (writers == null || !writers.hasNext()) + { + throw new IOException("no JPEG writer found"); + } + ImageWriter imageWriter = (ImageWriter) writers.next(); + + int numPoints = _track.getNumPoints(); + DataPoint point = null; + int photoNum = 0; + // Loop over all points in track + for (int i=0; i= _photos.length) + { + return null; + } + return _photos[inIndex]; + } +} diff --git a/tim/prune/save/PovExporter.java b/tim/prune/save/PovExporter.java index c1f90fd..3696dd1 100644 --- a/tim/prune/save/PovExporter.java +++ b/tim/prune/save/PovExporter.java @@ -21,9 +21,9 @@ import javax.swing.JTextField; import javax.swing.SwingConstants; import javax.swing.filechooser.FileFilter; -import tim.prune.App; import tim.prune.I18nManager; import tim.prune.data.Track; +import tim.prune.threedee.LineDialog; import tim.prune.threedee.ThreeDModel; /** @@ -32,7 +32,6 @@ import tim.prune.threedee.ThreeDModel; */ public class PovExporter { - private App _app = null; private JFrame _parentFrame = null; private Track _track = null; private JDialog _dialog = null; @@ -49,14 +48,12 @@ public class PovExporter /** - * Constructor giving App object, frame and track - * @param inApp application object to inform of success + * Constructor giving frame and track * @param inParentFrame parent frame * @param inTrack track object to save */ - public PovExporter(App inApp, JFrame inParentFrame, Track inTrack) + public PovExporter(JFrame inParentFrame, Track inTrack) { - _app = inApp; _parentFrame = inParentFrame; _track = inTrack; // Set default camera coordinates @@ -188,6 +185,22 @@ public class PovExporter JPanel flowPanel = new JPanel(); flowPanel.add(centralPanel); + + // show lines button + JButton showLinesButton = new JButton(I18nManager.getText("button.showlines")); + showLinesButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + // Need to scale model to find lines + ThreeDModel model = new ThreeDModel(_track); + model.scale(); + double[] latLines = model.getLatitudeLines(); + double[] lonLines = model.getLongitudeLines(); + LineDialog dialog = new LineDialog(_parentFrame, latLines, lonLines); + dialog.showDialog(); + } + }); + flowPanel.add(showLinesButton); panel.add(flowPanel, BorderLayout.CENTER); return panel; } @@ -204,7 +217,6 @@ public class PovExporter _cameraZ = checkCoordinate(_cameraZField.getText()); // OK pressed, so choose output file - boolean fileSaved = false; if (_fileChooser == null) _fileChooser = new JFileChooser(); _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG); @@ -244,7 +256,7 @@ public class PovExporter // Export the file if (exportFile(file)) { - fileSaved = true; + // file saved } else { @@ -479,14 +491,14 @@ public class PovExporter { inWriter.write("// Latitude and longitude lines:"); inWriter.write(inLineSeparator); - int numlines = inModel.getNumLatitudeLines(); + int numlines = inModel.getLatitudeLines().length; for (int i=0; i }"); inWriter.write(inLineSeparator); } - numlines = inModel.getNumLongitudeLines(); + numlines = inModel.getLongitudeLines().length; for (int i=0; i").append(I18nManager.getText("dialog.3dlines.empty")).append("

"); + } + else + { + descBuffer.append("

").append(I18nManager.getText("dialog.3dlines.intro")).append(":

"); + descBuffer.append("

").append(I18nManager.getText("fieldname.latitude")).append("

    "); + Latitude lat = null; + for (int i=0; i<_latLines.length; i++) + { + lat = new Latitude(_latLines[i], Latitude.FORMAT_DEG); + descBuffer.append("
  • ").append(lat.output(Latitude.FORMAT_DEG_WHOLE_MIN)).append("
  • "); + } + descBuffer.append("

"); + descBuffer.append("

").append(I18nManager.getText("fieldname.longitude")).append("

    "); + Longitude lon = null; + for (int i=0; i<_lonLines.length; i++) + { + lon = new Longitude(_lonLines[i], Longitude.FORMAT_DEG); + descBuffer.append("
  • ").append(lon.output(Longitude.FORMAT_DEG_WHOLE_MIN)).append("
  • "); + } + descBuffer.append("

"); + } + JEditorPane descPane = new JEditorPane("text/html", descBuffer.toString()); + descPane.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); + descPane.setEditable(false); + descPane.setOpaque(false); + panel.add(descPane, BorderLayout.CENTER); + // ok button + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + JButton okButton = new JButton(I18nManager.getText("button.ok")); + okButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _dialog.dispose(); + _dialog = null; + } + }); + buttonPanel.add(okButton); + panel.add(buttonPanel, BorderLayout.SOUTH); + return panel; + } +} diff --git a/tim/prune/threedee/ThreeDModel.java b/tim/prune/threedee/ThreeDModel.java index db748c0..141ec38 100644 --- a/tim/prune/threedee/ThreeDModel.java +++ b/tim/prune/threedee/ThreeDModel.java @@ -178,11 +178,11 @@ public class ThreeDModel /** - * @return number of latitude lines + * @return latitude lines */ - public int getNumLatitudeLines() + public double[] getLatitudeLines() { - return _scaler.getLatitudeLines().length; + return _scaler.getLatitudeLines(); } /** @@ -195,11 +195,11 @@ public class ThreeDModel } /** - * @return number of longitude lines + * @return longitude lines */ - public int getNumLongitudeLines() + public double[] getLongitudeLines() { - return _scaler.getLongitudeLines().length; + return _scaler.getLongitudeLines(); } /** diff --git a/tim/prune/threedee/WindowFactory.java b/tim/prune/threedee/WindowFactory.java index 89560ef..5a926b7 100644 --- a/tim/prune/threedee/WindowFactory.java +++ b/tim/prune/threedee/WindowFactory.java @@ -31,13 +31,13 @@ public abstract class WindowFactory /** * @return true if 3d capability is installed */ - private static boolean isJava3dEnabled() + public static boolean isJava3dEnabled() { boolean has3d = false; try { Class universeClass = Class.forName("com.sun.j3d.utils.universe.SimpleUniverse"); - has3d = true; + has3d = (universeClass != null); } catch (ClassNotFoundException e) { diff --git a/tim/prune/undo/UndoConnectPhoto.java b/tim/prune/undo/UndoConnectPhoto.java new file mode 100644 index 0000000..99e6432 --- /dev/null +++ b/tim/prune/undo/UndoConnectPhoto.java @@ -0,0 +1,58 @@ +package tim.prune.undo; + +import tim.prune.I18nManager; +import tim.prune.data.DataPoint; +import tim.prune.data.Photo; +import tim.prune.data.TrackInfo; + +/** + * Operation to undo the connection of a photo to a point + */ +public class UndoConnectPhoto implements UndoOperation +{ + private DataPoint _point = null; + private String _filename = null; + + + /** + * Constructor + * @param inPoint data point + * @param inFilename filename of photo + */ + public UndoConnectPhoto(DataPoint inPoint, String inFilename) + { + _point = inPoint; + _filename = inFilename; + } + + + /** + * @return description of operation including photo filename + */ + public String getDescription() + { + String desc = I18nManager.getText("undo.connectphoto") + " " + _filename; + return desc; + } + + + /** + * Perform the undo operation on the given Track + * @param inTrackInfo TrackInfo object on which to perform the operation + */ + public void performUndo(TrackInfo inTrackInfo) throws UndoException + { + // Disconnect again + Photo photo = _point.getPhoto(); + if (photo != null) + { + _point.setPhoto(null); + photo.setDataPoint(null); + } + else + { + // throw exception if failed + throw new UndoException(getDescription()); + } + } +} \ No newline at end of file diff --git a/tim/prune/undo/UndoDeletePhoto.java b/tim/prune/undo/UndoDeletePhoto.java new file mode 100644 index 0000000..cfc1662 --- /dev/null +++ b/tim/prune/undo/UndoDeletePhoto.java @@ -0,0 +1,73 @@ +package tim.prune.undo; + +import tim.prune.I18nManager; +import tim.prune.data.DataPoint; +import tim.prune.data.Photo; +import tim.prune.data.TrackInfo; + +/** + * Operation to undo a delete of a single photo, either with or without point + */ +public class UndoDeletePhoto implements UndoOperation +{ + private int _photoIndex = -1; + private Photo _photo = null; + private int _pointIndex = -1; + private DataPoint _point = null; + + + /** + * Constructor + * @param inPhoto photo + * @param inPhotoIndex index number of photo within photo list + * @param inPoint data point + * @param inPointIndex index number of point within track + */ + public UndoDeletePhoto(Photo inPhoto, int inPhotoIndex, DataPoint inPoint, int inPointIndex) + { + _photo = inPhoto; + _photoIndex = inPhotoIndex; + _point = inPoint; + _pointIndex = inPointIndex; + } + + + /** + * @return description of operation including photo name + */ + public String getDescription() + { + String desc = I18nManager.getText("undo.deletephoto") + " " + _photo.getFile().getName(); + return desc; + } + + + /** + * Perform the undo operation on the given Track + * @param inTrackInfo TrackInfo object on which to perform the operation + */ + public void performUndo(TrackInfo inTrackInfo) throws UndoException + { + // restore photo + inTrackInfo.getPhotoList().addPhoto(_photo, _photoIndex); + // if there's a point to restore, restore it + if (_point != null) + { + if (!inTrackInfo.getTrack().insertPoint(_point, _pointIndex)) + { + throw new UndoException(getDescription()); + } + } + else + { + // update needed if not already triggered by track update + inTrackInfo.triggerUpdate(); + } + // Ensure that photo is associated with point and vice versa + _photo.setDataPoint(_point); + if (_point != null) + { + _point.setPhoto(_photo); + } + } +} diff --git a/tim/prune/undo/UndoDeletePoint.java b/tim/prune/undo/UndoDeletePoint.java index 4cc09b4..daab9fd 100644 --- a/tim/prune/undo/UndoDeletePoint.java +++ b/tim/prune/undo/UndoDeletePoint.java @@ -11,17 +11,20 @@ public class UndoDeletePoint implements UndoOperation { private int _pointIndex = -1; private DataPoint _point = null; + private int _photoIndex = -1; /** * Constructor - * @param inIndex index number of point within track + * @param inPointIndex index number of point within track * @param inPoint data point + * @param inPhotoIndex index number of photo within photo list */ - public UndoDeletePoint(int inIndex, DataPoint inPoint) + public UndoDeletePoint(int inPointIndex, DataPoint inPoint, int inPhotoIndex) { - _pointIndex = inIndex; + _pointIndex = inPointIndex; _point = inPoint; + _photoIndex = inPhotoIndex; } @@ -40,7 +43,7 @@ public class UndoDeletePoint implements UndoOperation /** * Perform the undo operation on the given Track - * @param inTrack Track object on which to perform the operation + * @param inTrackInfo TrackInfo object on which to perform the operation */ public void performUndo(TrackInfo inTrackInfo) throws UndoException { @@ -49,6 +52,17 @@ public class UndoDeletePoint implements UndoOperation { throw new UndoException(getDescription()); } - // TODO: Reinsert photo into list if necessary + // Re-attach / Re-insert photo into list if necessary + if (_point.getPhoto() != null && _photoIndex > -1) + { + // Check if photo is still in list + if (!inTrackInfo.getPhotoList().contains(_point.getPhoto())) + { + // photo has been removed - need to reinsert + inTrackInfo.getPhotoList().addPhoto(_point.getPhoto(), _photoIndex); + } + // Ensure that photo is associated with point + _point.getPhoto().setDataPoint(_point); + } } -} \ No newline at end of file +} diff --git a/tim/prune/undo/UndoDeleteRange.java b/tim/prune/undo/UndoDeleteRange.java index 6bb0b7d..4e526cd 100644 --- a/tim/prune/undo/UndoDeleteRange.java +++ b/tim/prune/undo/UndoDeleteRange.java @@ -2,6 +2,7 @@ package tim.prune.undo; import tim.prune.I18nManager; import tim.prune.data.DataPoint; +import tim.prune.data.PhotoList; import tim.prune.data.TrackInfo; /** @@ -11,6 +12,7 @@ public class UndoDeleteRange implements UndoOperation { private int _startIndex = -1; private DataPoint[] _points = null; + private PhotoList _photoList = null; /** @@ -22,6 +24,7 @@ public class UndoDeleteRange implements UndoOperation { _startIndex = inTrackInfo.getSelection().getStart(); _points = inTrackInfo.cloneSelectedRange(); + _photoList = inTrackInfo.getPhotoList().cloneList(); } @@ -41,6 +44,17 @@ public class UndoDeleteRange implements UndoOperation */ public void performUndo(TrackInfo inTrackInfo) { + // restore photos to how they were before + inTrackInfo.getPhotoList().restore(_photoList); + // reconnect photos to points + for (int i=0; i<_points.length; i++) + { + DataPoint point = _points[i]; + if (point != null && point.getPhoto() != null) + { + point.getPhoto().setDataPoint(point); + } + } // restore point array into track inTrackInfo.getTrack().insertRange(_points, _startIndex); } diff --git a/tim/prune/undo/UndoEditPoint.java b/tim/prune/undo/UndoEditPoint.java index ae36865..a9f5665 100644 --- a/tim/prune/undo/UndoEditPoint.java +++ b/tim/prune/undo/UndoEditPoint.java @@ -41,7 +41,7 @@ public class UndoEditPoint implements UndoOperation /** * Perform the undo operation on the given Track - * @param inTrack Track object on which to perform the operation + * @param inTrackInfo TrackInfo object on which to perform the operation */ public void performUndo(TrackInfo inTrackInfo) throws UndoException { @@ -51,6 +51,5 @@ public class UndoEditPoint implements UndoOperation // throw exception if failed throw new UndoException(getDescription()); } - // TODO: Deal with photo if necessary } } \ No newline at end of file diff --git a/tim/prune/undo/UndoLoad.java b/tim/prune/undo/UndoLoad.java index 8bf2218..d354b9d 100644 --- a/tim/prune/undo/UndoLoad.java +++ b/tim/prune/undo/UndoLoad.java @@ -2,6 +2,7 @@ package tim.prune.undo; import tim.prune.I18nManager; import tim.prune.data.DataPoint; +import tim.prune.data.PhotoList; import tim.prune.data.TrackInfo; /** @@ -13,6 +14,7 @@ public class UndoLoad implements UndoOperation private int _numLoaded = -1; private DataPoint[] _contents = null; private String _previousFilename = null; + private PhotoList _photoList = null; /** @@ -33,14 +35,16 @@ public class UndoLoad implements UndoOperation * Constructor for replacing * @param inOldTrack track being replaced * @param inNumLoaded number of points loaded + * @param inPhotoList photo list, if any */ - public UndoLoad(TrackInfo inOldTrackInfo, int inNumLoaded) + public UndoLoad(TrackInfo inOldTrackInfo, int inNumLoaded, PhotoList inPhotoList) { _cropIndex = -1; _numLoaded = inNumLoaded; _contents = inOldTrackInfo.getTrack().cloneContents(); if (inOldTrackInfo.getFileInfo().getNumFiles() == 1) _previousFilename = inOldTrackInfo.getFileInfo().getFilename(); + _photoList = inPhotoList; } @@ -76,6 +80,11 @@ public class UndoLoad implements UndoOperation } else { + // replace photos how they were + if (_photoList != null) + { + inTrackInfo.getPhotoList().restore(_photoList); + } // replace track contents with old if (!inTrackInfo.getTrack().replaceContents(_contents)) { diff --git a/tim/prune/undo/UndoLoadPhotos.java b/tim/prune/undo/UndoLoadPhotos.java index c08532d..bb6a211 100644 --- a/tim/prune/undo/UndoLoadPhotos.java +++ b/tim/prune/undo/UndoLoadPhotos.java @@ -8,17 +8,19 @@ import tim.prune.data.TrackInfo; */ public class UndoLoadPhotos implements UndoOperation { - private int _numLoaded = -1; + private int _numPhotos = -1; + private int _numPoints = -1; - // TODO: Handle possibility of photos not having datapoints (yet) /** * Constructor - * @param inNumLoaded number of photos loaded + * @param inNumPhotos number of photos loaded + * @param inNumPoints number of points loaded */ - public UndoLoadPhotos(int inNumLoaded) + public UndoLoadPhotos(int inNumPhotos, int inNumPoints) { - _numLoaded = inNumLoaded; + _numPhotos = inNumPhotos; + _numPoints = inNumPoints; } @@ -28,8 +30,8 @@ public class UndoLoadPhotos implements UndoOperation public String getDescription() { String desc = I18nManager.getText("undo.loadphotos"); - if (_numLoaded > 0) - desc = desc + " (" + _numLoaded + ")"; + if (_numPhotos > 0) + desc = desc + " (" + _numPhotos + ")"; return desc; } @@ -41,12 +43,15 @@ public class UndoLoadPhotos implements UndoOperation */ public void performUndo(TrackInfo inTrackInfo) throws UndoException { + int cropIndex; // crop track to previous size - int cropIndex = inTrackInfo.getTrack().getNumPoints() - _numLoaded; - inTrackInfo.getTrack().cropTo(cropIndex); + if (_numPoints > 0) + { + cropIndex = inTrackInfo.getTrack().getNumPoints() - _numPoints; + inTrackInfo.getTrack().cropTo(cropIndex); + } // crop photo list to previous size - // (currently it is assumed that the number of points is the same as number of photos) - cropIndex = inTrackInfo.getPhotoList().getNumPhotos() - _numLoaded; + cropIndex = inTrackInfo.getPhotoList().getNumPhotos() - _numPhotos; inTrackInfo.getPhotoList().cropTo(cropIndex); // clear selection inTrackInfo.getSelection().clearAll(); -- 2.43.0