From 7f5ed2be62905bd37717376dc22d09e5ea7edb4d Mon Sep 17 00:00:00 2001 From: activityworkshop Date: Sun, 15 Feb 2015 11:46:36 +0100 Subject: [PATCH] Version 15, March 2013 --- tim/prune/App.java | 43 +- tim/prune/FileDropHandler.java | 158 ++++++ tim/prune/FunctionLibrary.java | 11 +- tim/prune/GpsPrune.java | 12 +- tim/prune/config/Config.java | 11 +- .../correlate/MediaPreviewTableModel.java | 14 +- tim/prune/data/Altitude.java | 98 ++-- tim/prune/data/AltitudeRange.java | 8 + tim/prune/data/Checker.java | 2 +- tim/prune/data/Coordinate.java | 8 +- tim/prune/data/DataPoint.java | 77 ++- tim/prune/data/DoubleRange.java | 18 + tim/prune/data/MediaList.java | 13 + tim/prune/data/MediaObject.java | 8 + tim/prune/data/NumberUtils.java | 16 +- tim/prune/data/PointCreateOptions.java | 72 +++ tim/prune/data/PointScaler.java | 283 ++-------- tim/prune/data/RangeStats.java | 269 +++++++++ tim/prune/data/Selection.java | 17 +- tim/prune/data/Speed.java | 102 ++++ tim/prune/data/SpeedCalculator.java | 20 +- tim/prune/data/Timestamp.java | 42 +- tim/prune/data/Track.java | 45 +- tim/prune/data/TrackExtents.java | 103 ++++ tim/prune/data/Unit.java | 13 +- tim/prune/data/UnitSet.java | 21 +- tim/prune/data/UnitSetLibrary.java | 21 +- tim/prune/function/AboutScreen.java | 2 +- tim/prune/function/AddAltitudeOffset.java | 13 +- tim/prune/function/AddMapSourceDialog.java | 18 + tim/prune/function/DownloadOsmFunction.java | 2 +- tim/prune/function/Export3dFunction.java | 2 +- tim/prune/function/FullRangeDetails.java | 185 ++----- tim/prune/function/GetWikipediaFunction.java | 52 +- tim/prune/function/PasteCoordinates.java | 10 +- tim/prune/function/SearchWikipediaNames.java | 104 +++- tim/prune/function/SetKmzImageSize.java | 159 ------ tim/prune/function/SetLanguage.java | 4 +- .../function/edit/EditFieldsTableModel.java | 54 +- tim/prune/function/edit/PointEditor.java | 205 +++++-- tim/prune/function/edit/PointNameEditor.java | 13 +- tim/prune/function/estimate/EstimateTime.java | 363 ++++++++++++ .../estimate/EstimationParameters.java | 278 ++++++++++ .../function/estimate/LearnParameters.java | 520 ++++++++++++++++++ .../function/estimate/ParametersPanel.java | 158 ++++++ tim/prune/function/estimate/jama/Maths.java | 31 ++ tim/prune/function/estimate/jama/Matrix.java | 259 +++++++++ .../estimate/jama/QRDecomposition.java | 219 ++++++++ .../gpsies/GenericDownloaderFunction.java | 3 + tim/prune/function/gpsies/TrackListModel.java | 6 + .../function/gpsies/UploadGpsiesFunction.java | 3 +- .../function/srtm/LookupSrtmFunction.java | 73 +-- tim/prune/function/srtm/TileFinder.java | 10 +- tim/prune/gui/DecimalNumberField.java | 111 ++++ tim/prune/gui/DetailsDisplay.java | 76 +-- tim/prune/gui/DisplayUtils.java | 45 ++ tim/prune/gui/GuiGridLayout.java | 44 +- tim/prune/gui/IconManager.java | 7 + tim/prune/gui/MenuManager.java | 36 +- tim/prune/gui/ProgressDialog.java | 115 ++++ tim/prune/gui/StatusIcon.java | 68 +++ tim/prune/gui/WholeNumberField.java | 2 +- tim/prune/gui/WizardLayout.java | 102 ++++ tim/prune/gui/images/add_photo_icon.png | Bin tim/prune/gui/images/add_textfile_icon.png | Bin tim/prune/gui/images/entry_invalid.gif | Bin 0 -> 84 bytes tim/prune/gui/images/entry_none.gif | Bin 0 -> 67 bytes tim/prune/gui/images/entry_valid.gif | Bin 0 -> 78 bytes tim/prune/gui/map/MapCanvas.java | 138 +++-- tim/prune/gui/map/MapUtils.java | 51 +- tim/prune/gui/profile/AltitudeData.java | 13 +- tim/prune/gui/profile/ArbitraryData.java | 79 +++ tim/prune/gui/profile/ProfileChart.java | 68 ++- tim/prune/lang/prune-texts_af.properties | 3 +- tim/prune/lang/prune-texts_cz.properties | 168 ++++-- tim/prune/lang/prune-texts_da.properties | 1 - tim/prune/lang/prune-texts_de.properties | 100 +++- tim/prune/lang/prune-texts_de_CH.properties | 98 +++- tim/prune/lang/prune-texts_en.properties | 103 +++- tim/prune/lang/prune-texts_en_US.properties | 1 + tim/prune/lang/prune-texts_es.properties | 10 +- tim/prune/lang/prune-texts_fr.properties | 18 +- tim/prune/lang/prune-texts_hu.properties | 9 +- tim/prune/lang/prune-texts_it.properties | 12 +- tim/prune/lang/prune-texts_ja.properties | 31 +- tim/prune/lang/prune-texts_ko.properties | 7 +- tim/prune/lang/prune-texts_nl.properties | 92 +++- tim/prune/lang/prune-texts_pl.properties | 80 ++- tim/prune/lang/prune-texts_pt.properties | 10 +- tim/prune/lang/prune-texts_ru.properties | 10 +- tim/prune/lang/prune-texts_tr.properties | 3 +- tim/prune/lang/prune-texts_zh.properties | 112 +++- tim/prune/load/BabelFileFormats.java | 3 +- tim/prune/load/BabelLoadFromFile.java | 16 +- tim/prune/load/BabelLoadFromGps.java | 12 + tim/prune/load/BabelLoader.java | 64 ++- tim/prune/load/ComponentHider.java | 59 ++ tim/prune/load/FieldGuesser.java | 50 +- tim/prune/load/FileCacher.java | 6 + tim/prune/load/FileLoader.java | 11 + tim/prune/load/JpegLoader.java | 3 +- tim/prune/load/MediaLoadProgressDialog.java | 1 - tim/prune/load/NmeaFileLoader.java | 4 +- tim/prune/load/TextFileLoader.java | 202 +++++-- tim/prune/load/babel/AddFilterDialog.java | 185 +++++++ tim/prune/load/babel/BabelFilterPanel.java | 184 +++++++ tim/prune/load/babel/DiscardFilter.java | 144 +++++ tim/prune/load/babel/DistanceFilter.java | 104 ++++ tim/prune/load/babel/FilterDefinition.java | 59 ++ tim/prune/load/babel/InterpolateFilter.java | 112 ++++ tim/prune/load/babel/SimplifyFilter.java | 147 +++++ tim/prune/load/xml/GzipFileLoader.java | 3 +- tim/prune/load/xml/XmlFileLoader.java | 4 +- tim/prune/load/xml/ZipFileLoader.java | 5 +- tim/prune/readme.txt | 24 +- tim/prune/save/BaseImageConfigDialog.java | 495 +++++++++++++++++ tim/prune/save/ExifSaver.java | 5 +- tim/prune/save/FileSaver.java | 17 +- tim/prune/save/GpxExporter.java | 8 +- tim/prune/save/GroutedImage.java | 111 ++++ tim/prune/save/ImageExporter.java | 454 +++++++++++++++ tim/prune/save/ImagePreviewPanel.java | 98 ++++ tim/prune/save/KmlExporter.java | 275 +++++++-- tim/prune/save/MapGrouter.java | 139 +++++ tim/prune/save/PovExporter.java | 255 +++++---- tim/prune/save/SvgExporter.java | 29 +- tim/prune/threedee/Java3DWindow.java | 95 +--- tim/prune/threedee/LineDialog.java | 113 ---- tim/prune/threedee/ThreeDModel.java | 90 +-- tim/prune/undo/UndoDeleteRange.java | 2 +- 130 files changed, 7882 insertions(+), 1780 deletions(-) create mode 100644 tim/prune/FileDropHandler.java create mode 100644 tim/prune/data/PointCreateOptions.java create mode 100644 tim/prune/data/RangeStats.java create mode 100644 tim/prune/data/Speed.java create mode 100644 tim/prune/data/TrackExtents.java delete mode 100644 tim/prune/function/SetKmzImageSize.java create mode 100644 tim/prune/function/estimate/EstimateTime.java create mode 100644 tim/prune/function/estimate/EstimationParameters.java create mode 100644 tim/prune/function/estimate/LearnParameters.java create mode 100644 tim/prune/function/estimate/ParametersPanel.java create mode 100644 tim/prune/function/estimate/jama/Maths.java create mode 100644 tim/prune/function/estimate/jama/Matrix.java create mode 100644 tim/prune/function/estimate/jama/QRDecomposition.java create mode 100644 tim/prune/gui/DecimalNumberField.java create mode 100644 tim/prune/gui/ProgressDialog.java create mode 100644 tim/prune/gui/StatusIcon.java create mode 100644 tim/prune/gui/WizardLayout.java mode change 100755 => 100644 tim/prune/gui/images/add_photo_icon.png mode change 100755 => 100644 tim/prune/gui/images/add_textfile_icon.png create mode 100644 tim/prune/gui/images/entry_invalid.gif create mode 100644 tim/prune/gui/images/entry_none.gif create mode 100644 tim/prune/gui/images/entry_valid.gif create mode 100644 tim/prune/gui/profile/ArbitraryData.java create mode 100644 tim/prune/load/ComponentHider.java create mode 100644 tim/prune/load/babel/AddFilterDialog.java create mode 100644 tim/prune/load/babel/BabelFilterPanel.java create mode 100644 tim/prune/load/babel/DiscardFilter.java create mode 100644 tim/prune/load/babel/DistanceFilter.java create mode 100644 tim/prune/load/babel/FilterDefinition.java create mode 100644 tim/prune/load/babel/InterpolateFilter.java create mode 100644 tim/prune/load/babel/SimplifyFilter.java create mode 100644 tim/prune/save/BaseImageConfigDialog.java create mode 100644 tim/prune/save/GroutedImage.java create mode 100644 tim/prune/save/ImageExporter.java create mode 100644 tim/prune/save/ImagePreviewPanel.java create mode 100644 tim/prune/save/MapGrouter.java delete mode 100644 tim/prune/threedee/LineDialog.java diff --git a/tim/prune/App.java b/tim/prune/App.java index 205d88a..1f22326 100644 --- a/tim/prune/App.java +++ b/tim/prune/App.java @@ -10,7 +10,6 @@ import javax.swing.JFrame; import javax.swing.JOptionPane; import tim.prune.config.Config; -import tim.prune.data.Altitude; import tim.prune.data.Checker; import tim.prune.data.DataPoint; import tim.prune.data.Field; @@ -18,11 +17,13 @@ import tim.prune.data.LatLonRectangle; import tim.prune.data.NumberUtils; import tim.prune.data.Photo; import tim.prune.data.PhotoList; +import tim.prune.data.PointCreateOptions; import tim.prune.data.RecentFile; import tim.prune.data.SourceInfo; import tim.prune.data.Track; import tim.prune.data.TrackInfo; import tim.prune.data.SourceInfo.FILE_TYPE; +import tim.prune.data.Unit; import tim.prune.function.AsyncMediaLoader; import tim.prune.function.SaveConfig; import tim.prune.function.SelectTracksFunction; @@ -107,7 +108,7 @@ public class App public boolean hasDataUnsaved() { return (_undoStack.size() > _lastSavePosition - && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().getNumPhotos() > 0)); + && (_track.getNumPoints() > 0 || _trackInfo.getPhotoList().hasModifiedMedia())); } /** @@ -403,12 +404,12 @@ public class App /** * Complete the add altitude offset function with the specified offset * @param inOffset altitude offset to add as String - * @param inFormat altitude format of offset (eg Feet, Metres) + * @param inUnit altitude units of offset (eg Feet, Metres) */ - public void finishAddAltitudeOffset(String inOffset, Altitude.Format inFormat) + public void finishAddAltitudeOffset(String inOffset, Unit inUnit) { // Sanity check - if (inOffset == null || inOffset.equals("") || inFormat==Altitude.Format.NO_FORMAT) { + if (inOffset == null || inOffset.equals("") || inUnit == null) { return; } // Construct undo information @@ -421,7 +422,7 @@ public class App // Decimal offset given try { double offsetd = Double.parseDouble(inOffset); - success = _trackInfo.getTrack().addAltitudeOffset(selStart, selEnd, offsetd, inFormat, numDecimals); + success = _trackInfo.getTrack().addAltitudeOffset(selStart, selEnd, offsetd, inUnit, numDecimals); } catch (NumberFormatException nfe) {} if (success) @@ -568,15 +569,14 @@ public class App * Receive loaded data and determine whether to filter on tracks or not * @param inFieldArray array of fields * @param inDataArray array of data - * @param inAltFormat altitude format * @param inSourceInfo information about the source of the data * @param inTrackNameList information about the track names */ public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, - Altitude.Format inAltFormat, SourceInfo inSourceInfo, TrackNameList inTrackNameList) + SourceInfo inSourceInfo, TrackNameList inTrackNameList) { // no link array given - informDataLoaded(inFieldArray, inDataArray, inAltFormat, inSourceInfo, + informDataLoaded(inFieldArray, inDataArray, null, inSourceInfo, inTrackNameList, null); } @@ -584,18 +584,33 @@ public class App * Receive loaded data and determine whether to filter on tracks or not * @param inFieldArray array of fields * @param inDataArray array of data - * @param inAltFormat altitude format + * @param inOptions creation options such as units * @param inSourceInfo information about the source of the data * @param inTrackNameList information about the track names - * @param inLinkInfo links to photo/audio clips */ public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, - Altitude.Format inAltFormat, SourceInfo inSourceInfo, - TrackNameList inTrackNameList, MediaLinkInfo inLinkInfo) + PointCreateOptions inOptions, SourceInfo inSourceInfo, TrackNameList inTrackNameList) + { + // no link array given + informDataLoaded(inFieldArray, inDataArray, inOptions, inSourceInfo, + inTrackNameList, null); + } + + /** + * Receive loaded data and determine whether to filter on tracks or not + * @param inFieldArray array of fields + * @param inDataArray array of data + * @param inOptions creation options such as units + * @param inSourceInfo information about the source of the data + * @param inTrackNameList information about the track names + * @param inLinkInfo links to photo/audio clips + */ + public void informDataLoaded(Field[] inFieldArray, Object[][] inDataArray, PointCreateOptions inOptions, + SourceInfo inSourceInfo, TrackNameList inTrackNameList, MediaLinkInfo inLinkInfo) { // Check whether loaded array can be properly parsed into a Track Track loadedTrack = new Track(); - loadedTrack.load(inFieldArray, inDataArray, inAltFormat); + loadedTrack.load(inFieldArray, inDataArray, inOptions); if (loadedTrack.getNumPoints() <= 0) { showErrorMessage("error.load.dialogtitle", "error.load.nopoints"); diff --git a/tim/prune/FileDropHandler.java b/tim/prune/FileDropHandler.java new file mode 100644 index 0000000..2c76385 --- /dev/null +++ b/tim/prune/FileDropHandler.java @@ -0,0 +1,158 @@ +package tim.prune; + +import java.awt.datatransfer.DataFlavor; +import java.io.File; +import java.net.URI; +import java.util.ArrayList; + +import javax.swing.TransferHandler; + +/** + * Class to listen for drag/drop events and react to dropping files on to the application + */ +public class FileDropHandler extends TransferHandler +{ + /** App object for passing results back to */ + private App _app = null; + + /** Fixed flavour in case the java file list flavour isn't available */ + private static DataFlavor _uriListFlavour = null; + + /** Static block to initialise the list flavour */ + static + { + try {_uriListFlavour = new DataFlavor("text/uri-list;class=java.lang.String"); + } catch (ClassNotFoundException nfe) {} + } + + + /** + * Constructor + * @param inApp App object to pass results of drop back to + */ + public FileDropHandler(App inApp) + { + _app = inApp; + } + + /** + * Check if the object being dragged can be accepted + * @param inSupport object to check + */ + public boolean canImport(TransferSupport inSupport) + { + boolean retval = inSupport.isDataFlavorSupported(DataFlavor.javaFileListFlavor) + || inSupport.isDataFlavorSupported(_uriListFlavour); + // Modify icon to show a copy, not a move (+ icon on cursor) + if (retval) { + inSupport.setDropAction(COPY); + } + return retval; + } + + /** + * Accept the incoming data and pass it on to the App + * @param inSupport contents of drop + */ + public boolean importData(TransferSupport inSupport) + { + if (!canImport(inSupport)) {return false;} // not allowed + + boolean success = false; + ArrayList dataFiles = new ArrayList(); + + // Try a java file list flavour first + try + { + @SuppressWarnings("unchecked") + java.util.List fileList = (java.util.List) + inSupport.getTransferable().getTransferData(DataFlavor.javaFileListFlavor); + success = true; + + for (File f : fileList) + { + if (f != null && f.exists()) + { + if (f.isDirectory()) { + addDirectoryToList(f, dataFiles); + } + else if (f.isFile()) { + dataFiles.add(f); + } + } + } + } catch (Exception e) {} // exception caught, probably missing a javafilelist flavour - just continue + + // If that didn't work, try a list of strings instead + if (!success) + { + try + { + String pathList = inSupport.getTransferable().getTransferData(_uriListFlavour).toString(); + success = true; + + for (String s : pathList.split("[\n\r]+")) + { + if (s != null && !s.equals("")) + { + File f = new File(new URI(s)); + if (f.exists()) + { + if (f.isDirectory()) { + addDirectoryToList(f, dataFiles); + } + else if (f.isFile()) { + dataFiles.add(f); + } + } + } + } + } catch (Exception e) { + System.err.println("exception: " + e.getClass().getName() + " - " + e.getMessage()); + return false; + } + } + + // Pass files back to app + if (!dataFiles.isEmpty()) { + _app.loadDataFiles(dataFiles); + } + return true; + } + + + /** + * Recursively-called method to add files from the given directory (and its subdirectories) + * to the given list of files + * @param inDir directory to add + * @param inList file list to append to + */ + private void addDirectoryToList(File inDir, ArrayList inList) + { + if (inDir != null && inDir.exists() && inDir.canRead() && inDir.isDirectory() && inList != null) + { + for (String path : inDir.list()) + { + if (path != null) + { + File f = new File(inDir, path); + if (f.exists() && f.canRead()) + { + if (f.isFile()) + { + // Add the found file to the list (if it's not in there already) + if (!inList.contains(f)) { + inList.add(f); + } + } + else if (f.isDirectory()) + { + // Recursively add the files from subdirectories + addDirectoryToList(f, inList); + } + } + } + } + } + } +} diff --git a/tim/prune/FunctionLibrary.java b/tim/prune/FunctionLibrary.java index f06dbe4..53d8563 100644 --- a/tim/prune/FunctionLibrary.java +++ b/tim/prune/FunctionLibrary.java @@ -7,6 +7,8 @@ import tim.prune.function.charts.Charter; import tim.prune.function.compress.CompressTrackFunction; import tim.prune.function.distance.DistanceFunction; import tim.prune.function.edit.PointNameEditor; +import tim.prune.function.estimate.EstimateTime; +import tim.prune.function.estimate.LearnParameters; import tim.prune.function.gpsies.GetGpsiesFunction; import tim.prune.function.gpsies.UploadGpsiesFunction; import tim.prune.function.srtm.LookupSrtmFunction; @@ -15,6 +17,7 @@ import tim.prune.load.BabelLoadFromFile; import tim.prune.load.BabelLoadFromGps; import tim.prune.save.GpsSaver; import tim.prune.save.GpxExporter; +import tim.prune.save.ImageExporter; import tim.prune.save.KmlExporter; import tim.prune.save.PovExporter; import tim.prune.save.SvgExporter; @@ -28,6 +31,7 @@ public abstract class FunctionLibrary public static GenericFunction FUNCTION_KMLEXPORT = null; public static PovExporter FUNCTION_POVEXPORT = null; public static SvgExporter FUNCTION_SVGEXPORT = null; + public static GenericFunction FUNCTION_IMAGEEXPORT = null; public static GenericFunction FUNCTION_GPSLOAD = null; public static GenericFunction FUNCTION_GPSSAVE = null; public static GenericFunction FUNCTION_IMPORTBABEL = null; @@ -63,6 +67,8 @@ public abstract class FunctionLibrary public static GenericFunction FUNCTION_3D = null; public static GenericFunction FUNCTION_DISTANCES = null; public static GenericFunction FUNCTION_FULL_RANGE_DETAILS = null; + public static GenericFunction FUNCTION_ESTIMATE_TIME = null; + public static GenericFunction FUNCTION_LEARN_ESTIMATION_PARAMS = null; public static GenericFunction FUNCTION_GET_GPSIES = null; public static GenericFunction FUNCTION_UPLOAD_GPSIES = null; public static GenericFunction FUNCTION_LOAD_AUDIO = null; @@ -73,7 +79,6 @@ public abstract class FunctionLibrary public static GenericFunction FUNCTION_SET_MAP_BG = null; public static GenericFunction FUNCTION_SET_DISK_CACHE = null; public static GenericFunction FUNCTION_SET_PATHS = null; - public static GenericFunction FUNCTION_SET_KMZ_IMAGE_SIZE = null; public static GenericFunction FUNCTION_SET_COLOURS = null; public static GenericFunction FUNCTION_SET_LINE_WIDTH = null; public static GenericFunction FUNCTION_SET_LANGUAGE = null; @@ -93,6 +98,7 @@ public abstract class FunctionLibrary FUNCTION_KMLEXPORT = new KmlExporter(inApp); FUNCTION_POVEXPORT = new PovExporter(inApp); FUNCTION_SVGEXPORT = new SvgExporter(inApp); + FUNCTION_IMAGEEXPORT = new ImageExporter(inApp); FUNCTION_GPSLOAD = new BabelLoadFromGps(inApp); FUNCTION_GPSSAVE = new GpsSaver(inApp); FUNCTION_IMPORTBABEL = new BabelLoadFromFile(inApp); @@ -127,6 +133,8 @@ public abstract class FunctionLibrary FUNCTION_3D = new ShowThreeDFunction(inApp); FUNCTION_DISTANCES = new DistanceFunction(inApp); FUNCTION_FULL_RANGE_DETAILS = new FullRangeDetails(inApp); + FUNCTION_ESTIMATE_TIME = new EstimateTime(inApp); + FUNCTION_LEARN_ESTIMATION_PARAMS = new LearnParameters(inApp); FUNCTION_GET_GPSIES = new GetGpsiesFunction(inApp); FUNCTION_UPLOAD_GPSIES = new UploadGpsiesFunction(inApp); FUNCTION_LOAD_AUDIO = new AudioLoader(inApp); @@ -138,7 +146,6 @@ public abstract class FunctionLibrary FUNCTION_SET_MAP_BG = new SetMapBgFunction(inApp); FUNCTION_SET_DISK_CACHE = new DiskCacheConfig(inApp); FUNCTION_SET_PATHS = new SetPathsFunction(inApp); - FUNCTION_SET_KMZ_IMAGE_SIZE = new SetKmzImageSize(inApp); FUNCTION_SET_COLOURS = new SetColours(inApp); FUNCTION_SET_LINE_WIDTH = new SetLineWidth(inApp); FUNCTION_SET_LANGUAGE = new SetLanguage(inApp); diff --git a/tim/prune/GpsPrune.java b/tim/prune/GpsPrune.java index 70e1c8e..3214bac 100644 --- a/tim/prune/GpsPrune.java +++ b/tim/prune/GpsPrune.java @@ -35,9 +35,9 @@ import tim.prune.gui.profile.ProfileChart; public class GpsPrune { /** Version number of application, used in about screen and for version check */ - public static final String VERSION_NUMBER = "14.1"; + public static final String VERSION_NUMBER = "15"; /** Build number, just used for about screen */ - public static final String BUILD_NUMBER = "265a"; + public static final String BUILD_NUMBER = "283"; /** Static reference to App object */ private static App APP = null; @@ -98,8 +98,9 @@ public class GpsPrune } } } - if (showUsage) { - System.out.println("Possible parameters:" + if (showUsage) + { + System.out.println("GpsPrune - a tool for editing GPS data.\nPossible parameters:" + "\n --configfile= used to specify a configuration file" + "\n --lang= used to specify language code such as DE" + "\n --langfile= used to specify an alternative language file\n"); @@ -233,6 +234,9 @@ public class GpsPrune } catch (Exception e) {} // ignore + // Set up drag-and-drop handler to accept dropped files + frame.setTransferHandler(new FileDropHandler(APP)); + // finish off and display frame frame.pack(); frame.setSize(650, 450); diff --git a/tim/prune/config/Config.java b/tim/prune/config/Config.java index 5a95beb..385a5d9 100644 --- a/tim/prune/config/Config.java +++ b/tim/prune/config/Config.java @@ -42,6 +42,8 @@ public abstract class Config public static final String KEY_GPS_DEVICE = "prune.gpsdevice"; /** Key for GPS format */ public static final String KEY_GPS_FORMAT = "prune.gpsformat"; + /** Key for GPSBabel filter string */ + public static final String KEY_GPSBABEL_FILTER = "prune.gpsbabelfilter"; /** Key for Povray font */ public static final String KEY_POVRAY_FONT = "prune.povrayfont"; /** Key for the selected unit set */ @@ -59,9 +61,7 @@ public abstract class Config /** Key for working online flag */ public static final String KEY_ONLINE_MODE = "prune.onlinemode"; /** Key for width of thumbnails in kmz */ - public static final String KEY_KMZ_IMAGE_WIDTH = "prune.kmzimagewidth"; - /** Key for height of thumbnails in kmz */ - public static final String KEY_KMZ_IMAGE_HEIGHT = "prune.kmzimageheight"; + public static final String KEY_KMZ_IMAGE_SIZE = "prune.kmzimagewidth"; /** Key for gpsbabel path */ public static final String KEY_GPSBABEL_PATH = "prune.gpsbabelpath"; /** Key for gnuplot path */ @@ -78,6 +78,8 @@ public abstract class Config public static final String KEY_AUTOSAVE_SETTINGS = "prune.autosavesettings"; /** Key for recently used files */ public static final String KEY_RECENT_FILES = "prune.recentfiles"; + /** Key for estimation parameters */ + public static final String KEY_ESTIMATION_PARAMS = "prune.estimationparams"; /** Initialise the default properties */ @@ -162,8 +164,7 @@ public abstract class Config props.put(KEY_EXIFTOOL_PATH, "exiftool"); props.put(KEY_GNUPLOT_PATH, "gnuplot"); props.put(KEY_GPSBABEL_PATH, "gpsbabel"); - props.put(KEY_KMZ_IMAGE_WIDTH, "240"); - props.put(KEY_KMZ_IMAGE_HEIGHT, "240"); + props.put(KEY_KMZ_IMAGE_SIZE, "240"); props.put(KEY_AUTOSAVE_SETTINGS, "0"); // autosave false by default props.put(KEY_UNITSET_KEY, "unitset.kilometres"); // metric by default return props; diff --git a/tim/prune/correlate/MediaPreviewTableModel.java b/tim/prune/correlate/MediaPreviewTableModel.java index 3d6efa8..e562ff9 100644 --- a/tim/prune/correlate/MediaPreviewTableModel.java +++ b/tim/prune/correlate/MediaPreviewTableModel.java @@ -1,11 +1,12 @@ package tim.prune.correlate; -import java.text.NumberFormat; import java.util.ArrayList; import javax.swing.table.AbstractTableModel; + import tim.prune.I18nManager; import tim.prune.data.Unit; import tim.prune.data.UnitSetLibrary; +import tim.prune.gui.DisplayUtils; /** * Class to act as the table model for the correlation preview table @@ -18,16 +19,7 @@ public class MediaPreviewTableModel extends AbstractTableModel private ArrayList _list = new ArrayList(); /** Distance units */ private Unit _distanceUnits = UnitSetLibrary.UNITS_KILOMETRES; - /** Number formatter */ - private static final NumberFormat FORMAT_ONE_DP = NumberFormat.getNumberInstance(); - - /** Static block to initialise the one d.p. formatter */ - static - { - FORMAT_ONE_DP.setMaximumFractionDigits(1); - FORMAT_ONE_DP.setMinimumFractionDigits(1); - } /** * Constructor @@ -103,7 +95,7 @@ public class MediaPreviewTableModel extends AbstractTableModel } else if (inColumnIndex == 3) { if (row.getPointPair().isValid()) { - return FORMAT_ONE_DP.format(row.getDistance(_distanceUnits)); + return DisplayUtils.formatOneDp(row.getDistance(_distanceUnits)); } return ""; } diff --git a/tim/prune/data/Altitude.java b/tim/prune/data/Altitude.java index ac71d75..7d9c21c 100644 --- a/tim/prune/data/Altitude.java +++ b/tim/prune/data/Altitude.java @@ -7,41 +7,26 @@ public class Altitude { private boolean _valid = false; private int _value = 0; - private Format _format = Format.NO_FORMAT; + private Unit _unit = null; private String _stringValue = null; - /** Altitude formats */ - public enum Format { - /** No format */ - NO_FORMAT, - /** Metres */ - METRES, - /** Feet */ - FEET - } - - /** Constants for conversion */ - private static final double CONVERT_METRES_TO_FEET = UnitSetLibrary.UNITS_FEET.getMultFactorFromStd(); - private static final double CONVERT_FEET_TO_METRES = 1 / CONVERT_METRES_TO_FEET; - - /** Constant for no altitude value */ - public static final Altitude NONE = new Altitude(null, Format.NO_FORMAT); - + /** Constant to use for a lack of altitude */ + public static final Altitude NONE = new Altitude(null, null); /** * Constructor using String * @param inString string to parse - * @param inFormat format of altitude, either metres or feet + * @param inUnit of altitude, either metres or feet */ - public Altitude(String inString, Format inFormat) + public Altitude(String inString, Unit inUnit) { + _unit = inUnit; if (inString != null && !inString.equals("")) { try { _stringValue = inString; _value = (int) Double.parseDouble(inString.trim()); - _format = inFormat; _valid = true; } catch (NumberFormatException nfe) {} @@ -52,13 +37,14 @@ public class Altitude /** * Constructor with int value * @param inValue int value of altitude - * @param inFormat format of altitude, either metres or feet + * @param inUnit unit of altitude, either metres or feet */ - public Altitude(int inValue, Format inFormat) + public Altitude(int inValue, Unit inUnit) { _value = inValue; - _format = inFormat; + _unit = inUnit; _valid = true; + _stringValue = "" + inValue; } /** @@ -66,7 +52,7 @@ public class Altitude */ public Altitude clone() { - return new Altitude(_stringValue, _format); + return new Altitude(_stringValue, _unit); } /** @@ -77,7 +63,7 @@ public class Altitude { _stringValue = inClone._stringValue; _value = inClone._value; - _format = inClone._format; + _unit = inClone._unit; _valid = inClone._valid; } @@ -104,33 +90,18 @@ public class Altitude */ public int getValue(Unit inAltUnit) { + if (inAltUnit == null) { + return getValue(); + } return (int) (getMetricValue() * inAltUnit.getMultFactorFromStd()); } /** - * @return format of number + * @return unit of number */ - public Format getFormat() + public Unit getUnit() { - return _format; - } - - - /** - * Get the altitude value in the specified format - * @param inFormat desired format, either FORMAT_METRES or FORMAT_FEET - * @return value as an int - */ - public int getValue(Format inFormat) - { - // Note possible rounding errors here if converting to/from units - if (inFormat == _format) - return _value; - if (inFormat == Format.METRES) - return (int) (_value * CONVERT_FEET_TO_METRES); - if (inFormat == Format.FEET) - return (int) (_value * CONVERT_METRES_TO_FEET); - return _value; + return _unit; } /** @@ -138,26 +109,26 @@ public class Altitude */ public double getMetricValue() { - if (_format == Format.FEET) { - return _value / UnitSetLibrary.UNITS_FEET.getMultFactorFromStd(); + if (_unit == UnitSetLibrary.UNITS_METRES || _unit == null) { + return _value; } - return _value; + return _value / _unit.getMultFactorFromStd(); } /** * Get a string version of the value - * @param inFormat specified format + * @param inUnit specified unit * @return string value, if possible the original one */ - public String getStringValue(Format inFormat) + public String getStringValue(Unit inUnit) { if (!_valid) {return "";} // Return string value if the same format or "no format" was requested - if ((inFormat == _format || inFormat == Format.NO_FORMAT) + if ((inUnit == _unit || inUnit == null) && _stringValue != null && !_stringValue.equals("")) { return _stringValue; } - return "" + getValue(inFormat); + return "" + getValue(inUnit); } @@ -188,39 +159,36 @@ public class Altitude if (inStart == null || inEnd == null || !inStart.isValid() || !inEnd.isValid()) return Altitude.NONE; // Use altitude format of first point - Format altFormat = inStart.getFormat(); + Unit altUnit = inStart.getUnit(); int startValue = inStart.getValue(); - int endValue = inEnd.getValue(altFormat); + int endValue = inEnd.getValue(altUnit); // interpolate between start and end int newValue = startValue + (int) ((endValue - startValue) * inFrac); - return new Altitude(newValue, altFormat); + return new Altitude(newValue, altUnit); } /** * Add the given offset to the current altitude * @param inOffset offset as double - * @param inFormat format of offset, feet or metres + * @param inUnit unit of offset, feet or metres * @param inDecimals number of decimal places */ - public void addOffset(double inOffset, Format inFormat, int inDecimals) + public void addOffset(double inOffset, Unit inUnit, int inDecimals) { // Use the maximum number of decimal places from current value and offset int numDecimals = NumberUtils.getDecimalPlaces(_stringValue); if (numDecimals < inDecimals) {numDecimals = inDecimals;} // Convert offset to correct units double offset = inOffset; - if (inFormat != _format) + if (inUnit != _unit && inUnit != null) { - if (inFormat == Format.FEET) - offset = inOffset * CONVERT_FEET_TO_METRES; - else - offset = inOffset * CONVERT_METRES_TO_FEET; + offset = inOffset / inUnit.getMultFactorFromStd() * _unit.getMultFactorFromStd(); } // FIXME: The following will fail if _stringValue is null - not sure how it can get in that state! if (_stringValue == null) System.err.println("*** Altitude.addOffset - how did the string value get to be null?"); // Add the offset double newValue = Double.parseDouble(_stringValue.trim()) + offset; _value = (int) newValue; - _stringValue = NumberUtils.formatNumber(newValue, numDecimals); + _stringValue = NumberUtils.formatNumberUk(newValue, numDecimals); } } diff --git a/tim/prune/data/AltitudeRange.java b/tim/prune/data/AltitudeRange.java index b7ec647..d53223e 100644 --- a/tim/prune/data/AltitudeRange.java +++ b/tim/prune/data/AltitudeRange.java @@ -118,4 +118,12 @@ public class AltitudeRange { return (int) (_descent * inUnit.getMultFactorFromStd()); } + + /** + * @return overall height gain in metres + */ + public double getMetricHeightDiff() + { + return _climb - _descent; + } } diff --git a/tim/prune/data/Checker.java b/tim/prune/data/Checker.java index 274c3e7..085f50f 100644 --- a/tim/prune/data/Checker.java +++ b/tim/prune/data/Checker.java @@ -18,7 +18,7 @@ public abstract class Checker if (inTrack == null || inTrack.getNumPoints() < 2) {return false;} // Check for non-even number of points final int numPoints = inTrack.getNumPoints(); - if (numPoints % 2 == 1) {return false;} + if (numPoints % 2 != 0) {return false;} // Loop through first half of track final int halfNum = numPoints / 2; for (int i=0; i= '0' && currChar <= '9') { - if (!inNumeric) + if (!isNumeric) { - inNumeric = true; + isNumeric = true; numFields++; denoms[numFields-1] = 1; } @@ -111,7 +111,7 @@ public abstract class Coordinate } else { - inNumeric = false; + isNumeric = false; // Remember delimiters if (currChar != ',' && currChar != '.') {otherDelims[numFields] = true;} } diff --git a/tim/prune/data/DataPoint.java b/tim/prune/data/DataPoint.java index 34d7bd7..4de97ef 100644 --- a/tim/prune/data/DataPoint.java +++ b/tim/prune/data/DataPoint.java @@ -15,7 +15,8 @@ public class DataPoint private FieldList _fieldList = null; /** Special fields for coordinates */ private Coordinate _latitude = null, _longitude = null; - private Altitude _altitude; + private Altitude _altitude = null; + private Speed _hSpeed = null, _vSpeed = null; private Timestamp _timestamp = null; /** Attached photo */ private Photo _photo = null; @@ -26,13 +27,14 @@ public class DataPoint private boolean _markedForDeletion = false; private int _modifyCount = 0; + /** * Constructor * @param inValueArray array of String values * @param inFieldList list of fields - * @param inAltFormat altitude format + * @param inOptions creation options such as units */ - public DataPoint(String[] inValueArray, FieldList inFieldList, Altitude.Format inAltFormat) + public DataPoint(String[] inValueArray, FieldList inFieldList, PointCreateOptions inOptions) { // save data _fieldValues = inValueArray; @@ -41,25 +43,42 @@ public class DataPoint // Remove double quotes around values removeQuotes(_fieldValues); // parse fields into objects - parseFields(null, inAltFormat); + parseFields(null, inOptions); } /** * Parse the string values into objects eg Coordinates * @param inField field which has changed, or null for all - * @param inAltFormat altitude format + * @param inOptions creation options such as units */ - private void parseFields(Field inField, Altitude.Format inAltFormat) + private void parseFields(Field inField, PointCreateOptions inOptions) { + if (inOptions == null) inOptions = new PointCreateOptions(); if (inField == null || inField == Field.LATITUDE) { _latitude = new Latitude(getFieldValue(Field.LATITUDE)); } if (inField == null || inField == Field.LONGITUDE) { _longitude = new Longitude(getFieldValue(Field.LONGITUDE)); } - if (inField == null || inField == Field.ALTITUDE) { - _altitude = new Altitude(getFieldValue(Field.ALTITUDE), inAltFormat); + if (inField == null || inField == Field.ALTITUDE) + { + Unit altUnit = inOptions.getAltitudeUnits(); + if (_altitude != null && _altitude.getUnit() != null) { + altUnit = _altitude.getUnit(); + } + _altitude = new Altitude(getFieldValue(Field.ALTITUDE), altUnit); + } + if (inField == null || inField == Field.SPEED) + { + _hSpeed = new Speed(getFieldValue(Field.SPEED), inOptions.getSpeedUnits()); + } + if (inField == null || inField == Field.VERTICAL_SPEED) + { + _vSpeed = new Speed(getFieldValue(Field.VERTICAL_SPEED), inOptions.getVerticalSpeedUnits()); + if (!inOptions.getVerticalSpeedsUpwards()) { + _vSpeed.invert(); + } } if (inField == null || inField == Field.TIMESTAMP) { _timestamp = new Timestamp(getFieldValue(Field.TIMESTAMP)); @@ -160,13 +179,13 @@ public class DataPoint setModified(inUndo); } // Change Coordinate, Altitude, Name or Timestamp fields after edit - if (_altitude != null && _altitude.getFormat() != Altitude.Format.NO_FORMAT) { + if (_altitude != null && _altitude.getUnit() != null) { // Altitude already present so reuse format - parseFields(inField, _altitude.getFormat()); + parseFields(inField, null); // current units will be used } else { // use default altitude format from config - parseFields(inField, Config.getUnitSet().getDefaultAltitudeFormat()); + parseFields(inField, Config.getUnitSet().getDefaultOptions()); } } @@ -219,13 +238,33 @@ public class DataPoint /** @return true if point has altitude */ public boolean hasAltitude() { - return _altitude.isValid(); + return _altitude != null && _altitude.isValid(); } /** @return altitude */ public Altitude getAltitude() { return _altitude; } + /** @return true if point has horizontal speed (loaded as field) */ + public boolean hasHSpeed() + { + return _hSpeed != null && _hSpeed.isValid(); + } + /** @return horizontal speed */ + public Speed getHSpeed() + { + return _hSpeed; + } + /** @return true if point has vertical speed (loaded as field) */ + public boolean hasVSpeed() + { + return _vSpeed != null && _vSpeed.isValid(); + } + /** @return vertical speed */ + public Speed getVSpeed() + { + return _vSpeed; + } /** @return true if point has timestamp */ public boolean hasTimestamp() { @@ -472,8 +511,20 @@ public class DataPoint // Copy all values (note that photo not copied) String[] valuesCopy = new String[_fieldValues.length]; System.arraycopy(_fieldValues, 0, valuesCopy, 0, _fieldValues.length); + + PointCreateOptions options = new PointCreateOptions(); + if (_altitude != null) { + options.setAltitudeUnits(_altitude.getUnit()); + } // Make new object to hold cloned data - DataPoint point = new DataPoint(valuesCopy, _fieldList, _altitude.getFormat()); + DataPoint point = new DataPoint(valuesCopy, _fieldList, options); + // Copy the speed information + if (hasHSpeed()) { + point.getHSpeed().copyFrom(_hSpeed); + } + if (hasVSpeed()) { + point.getVSpeed().copyFrom(_vSpeed); + } return point; } diff --git a/tim/prune/data/DoubleRange.java b/tim/prune/data/DoubleRange.java index b6b8a8f..4e36122 100644 --- a/tim/prune/data/DoubleRange.java +++ b/tim/prune/data/DoubleRange.java @@ -79,4 +79,22 @@ public class DoubleRange { return _max - _min; } + + /** + * @return mid value, halfway between min and max + */ + public double getMidValue() + { + return (_max + _min) / 2.0; + } + + /** + * Copy this range into a new object, which can then be modified without changing this one + * @return deep copy of this object + */ + public DoubleRange copy() + { + if (_empty) return new DoubleRange(); + return new DoubleRange(_min, _max); + } } diff --git a/tim/prune/data/MediaList.java b/tim/prune/data/MediaList.java index c0f4408..cc30325 100644 --- a/tim/prune/data/MediaList.java +++ b/tim/prune/data/MediaList.java @@ -219,6 +219,19 @@ public abstract class MediaList return false; } + /** + * @return true if there are any modified media in the list + */ + public boolean hasModifiedMedia() + { + for (MediaObject m: _media) { + if (m.isModified()) { + return true; + } + } + return false; + } + /** * @return clone of list contents */ diff --git a/tim/prune/data/MediaObject.java b/tim/prune/data/MediaObject.java index 478618e..8c53caf 100644 --- a/tim/prune/data/MediaObject.java +++ b/tim/prune/data/MediaObject.java @@ -211,6 +211,14 @@ public abstract class MediaObject return _currentStatus != Status.NOT_CONNECTED; } + /** + * @return true if status has changed since load + */ + public boolean isModified() + { + return _currentStatus != _originalStatus; + } + /** * Reset any cached data (eg thumbnail) */ diff --git a/tim/prune/data/NumberUtils.java b/tim/prune/data/NumberUtils.java index 0c76981..652e824 100644 --- a/tim/prune/data/NumberUtils.java +++ b/tim/prune/data/NumberUtils.java @@ -9,11 +9,11 @@ import java.util.Locale; */ public abstract class NumberUtils { - /** Number formatter object to avoid lots of instantiations */ - private static final NumberFormat NUM_FORMATTER = NumberFormat.getNumberInstance(Locale.UK); + /** UK-specific number formatter object to avoid lots of instantiations */ + private static final NumberFormat UK_FORMAT = NumberFormat.getNumberInstance(Locale.UK); // Select the UK locale for this formatter so that decimal point is always used (not comma) static { - if (NUM_FORMATTER instanceof DecimalFormat) ((DecimalFormat) NUM_FORMATTER).applyPattern("0.000"); + if (UK_FORMAT instanceof DecimalFormat) ((DecimalFormat) UK_FORMAT).applyPattern("0.000"); } /** @@ -42,14 +42,14 @@ public abstract class NumberUtils } /** - * Format the given number to the given number of decimal places + * Format the given number in UK format (decimal point) to the given number of decimal places * @param inNumber double number to format * @param inDecimalPlaces number of decimal places */ - public static String formatNumber(double inNumber, int inDecimalPlaces) + public static String formatNumberUk(double inNumber, int inDecimalPlaces) { - NUM_FORMATTER.setMaximumFractionDigits(inDecimalPlaces); - NUM_FORMATTER.setMinimumFractionDigits(inDecimalPlaces); - return NUM_FORMATTER.format(inNumber); + UK_FORMAT.setMaximumFractionDigits(inDecimalPlaces); + UK_FORMAT.setMinimumFractionDigits(inDecimalPlaces); + return UK_FORMAT.format(inNumber); } } \ No newline at end of file diff --git a/tim/prune/data/PointCreateOptions.java b/tim/prune/data/PointCreateOptions.java new file mode 100644 index 0000000..2f61336 --- /dev/null +++ b/tim/prune/data/PointCreateOptions.java @@ -0,0 +1,72 @@ +package tim.prune.data; + +/** + * Class to hold the options when creating (or loading) a new point, + * such as units for altitudes, speeds and vertical speeds + */ +public class PointCreateOptions +{ + private Unit _altitudeUnit = UnitSetLibrary.UNITS_METRES; + private Unit _speedUnit = UnitSetLibrary.SPEED_UNITS_METRESPERSEC; + private Unit _vertSpeedUnit = UnitSetLibrary.SPEED_UNITS_METRESPERSEC; + private boolean _vertSpeedsUpwards = true; + + /** + * @param inUnit altitude units (only metres or feet accepted) + */ + public void setAltitudeUnits(Unit inUnit) + { + if (inUnit == UnitSetLibrary.UNITS_METRES || inUnit == UnitSetLibrary.UNITS_FEET) { + _altitudeUnit = inUnit; + } + } + + /** @return altitude units */ + public Unit getAltitudeUnits() {return _altitudeUnit;} + + /** + * @param inUnit speed units (only m/s, ft/s, km/h and mph accepted) + */ + public void setSpeedUnits(Unit inUnit) + { + if (inUnit == UnitSetLibrary.SPEED_UNITS_METRESPERSEC + || inUnit == UnitSetLibrary.SPEED_UNITS_FEETPERSEC + || inUnit == UnitSetLibrary.SPEED_UNITS_KMPERHOUR + || inUnit == UnitSetLibrary.SPEED_UNITS_MILESPERHOUR) + { + _speedUnit = inUnit; + } + } + + /** @return speed units */ + public Unit getSpeedUnits() {return _speedUnit;} + + /** + * @param inUnit speed units (only m/s, ft/s, km/h and mph accepted) + * @param inUpwards true if positive speeds are upwards, negative downwards + */ + public void setVerticalSpeedUnits(Unit inUnit, boolean inUpwards) + { + if (inUnit == UnitSetLibrary.SPEED_UNITS_METRESPERSEC + || inUnit == UnitSetLibrary.SPEED_UNITS_FEETPERSEC + || inUnit == UnitSetLibrary.SPEED_UNITS_KMPERHOUR + || inUnit == UnitSetLibrary.SPEED_UNITS_MILESPERHOUR) + { + _vertSpeedUnit = inUnit; + _vertSpeedsUpwards = inUpwards; + } + } + + /** @return vertical speed units */ + public Unit getVerticalSpeedUnits() {return _vertSpeedUnit;} + + /** @return true if positive speeds are upwards, negative downwards */ + public boolean getVerticalSpeedsUpwards() {return _vertSpeedsUpwards;} + + /** for debug */ + public String toString() + { + return "options: altitude " + _altitudeUnit.getNameKey() + ", speed " + _speedUnit.getNameKey() + + ", vspeed " + _vertSpeedUnit.getNameKey() + (_vertSpeedsUpwards ? " (upwards)" : " (downwards)"); + } +} diff --git a/tim/prune/data/PointScaler.java b/tim/prune/data/PointScaler.java index 180f3a5..a133885 100644 --- a/tim/prune/data/PointScaler.java +++ b/tim/prune/data/PointScaler.java @@ -1,41 +1,19 @@ package tim.prune.data; /** - * Class to manage the scaling of points + * Class to manage the scaling of points, used by the ThreeDModel */ public class PointScaler { // Original data private Track _track = null; - // Range information - private double _latMedian = 0.0; - private double _lonMedian = 0.0; - private int _minAltitude = 0; - // Scaling information - private double _longFactor = 0.0; - private double _altFactor = 0.0; - // Scaled points + // Scaled values private double[] _xValues = null; private double[] _yValues = null; private double[] _altValues = null; - // max values - private double _maxX = 0.0; - private double _maxY = 0.0; - private double _maxScaledAlt = 0.0; - // lat/long lines - private double[] _latLinesDegs = null; - private double[] _lonLinesDegs = null; - private double[] _latLinesScaled = null; - private double[] _lonLinesScaled = null; + // Altitude range + private double _altitudeRange = 0.0; - // Constants - private static final double[] COORD_SEPARATIONS = { - 1.0, // 1deg - 30.0/60.0, 20.0/60.0, // 30min, 20min - 10.0/60.0, 5.0/60.0, // 10min, 5min - 3.0/60.0, 2.0/60.0, 1.0/60.0 // 3min, 2min, 1min - }; - private static final int MAX_COORD_SEPARATION_INDEX = COORD_SEPARATIONS.length - 1; /** * Constructor @@ -52,78 +30,42 @@ public class PointScaler */ public void scale() { - // Clear data - DoubleRange latRange = new DoubleRange(); - DoubleRange lonRange = new DoubleRange(); - DoubleRange altRange = new DoubleRange(); - int numPoints = 0; - int p = 0; - DataPoint point = null; - // Find limits of data - if (_track != null && (numPoints = _track.getNumPoints()) > 0) + // Work out extents + TrackExtents extents = new TrackExtents(_track); + extents.applySquareBorder(); + final double horizDistance = Math.max(extents.getHorizontalDistanceMetres(), 1.0); + final int numPoints = _track.getNumPoints(); + + // Find altitude range + _altitudeRange = extents.getAltitudeRange().getRange() / horizDistance; + final double minAltitude = extents.getAltitudeRange().getMinimum(); + + // create new arrays for scaled values + if (_xValues == null || _xValues.length != numPoints) { - for (p=0; p _maxScaledAlt) {_maxScaledAlt = _altValues[p];} - } + _xValues[p] = (_track.getX(p) - midXvalue) / xyRange; + _yValues[p] = (midYvalue - _track.getY(p)) / xyRange; // y values have to be inverted + _altValues[p] = (point.getAltitude().getMetricValue() - minAltitude) / horizDistance; } - // Calculate x and y range - _maxX = getScaledLongitude(lonRange.getMaximum()); - _maxY = getScaledLatitude(latRange.getMaximum()); } } - /** - * @return maximum horiz value - */ - public double getMaximumHoriz() { return _maxX; } - /** - * @return maximum vert value - */ - public double getMaximumVert() { return _maxY; } - - /** @return maximum scaled altitude value */ - public double getMaxScaledAlt() { return _maxScaledAlt; } /** * Get the horizontal value for the specified point @@ -134,6 +76,7 @@ public class PointScaler { return _xValues[inIndex]; } + /** * Get the vertical value for the specified point * @param inIndex index of point, starting at 0 @@ -143,6 +86,7 @@ public class PointScaler { return _yValues[inIndex]; } + /** * Get the altitude value for the specified point * @param inIndex index of point, starting at 0 @@ -154,165 +98,10 @@ public class PointScaler } /** - * Scale the given latitude value - * @param inLatitude latitude in degrees - * @return scaled latitude - */ - private double getScaledLatitude(double inLatitude) - { - return inLatitude - _latMedian; - } - /** - * Scale the given longitude value - * @param inLongitude longitude in degrees - * @return scaled longitude - */ - private double getScaledLongitude(double inLongitude) - { - return (inLongitude - _lonMedian) * _longFactor; - } - /** - * Scale the given altitude value - * @param inAltitude Altitude object - * @return scaled altitude - */ - private double getScaledAltitude(Altitude inAltitude) - { - if (inAltitude == null) return -1; - return (inAltitude.getValue(Altitude.Format.METRES) - _minAltitude) * _altFactor; - } - - /** - * Unscale the given latitude value - * @param inScaledLatitude scaled latitude - * @return latitude in degrees - */ - private double getUnscaledLatitude(double inScaledLatitude) - { - return inScaledLatitude + _latMedian; - } - /** - * Unscale the given longitude value - * @param inScaledLongitude scaled longitude - * @return longitude in degrees - */ - private double getUnscaledLongitude(double inScaledLongitude) - { - return inScaledLongitude / _longFactor + _lonMedian; - } - - /** - * Calculate the latitude and longitude lines - */ - public void calculateLatLongLines() - { - double maxValue = getMaximumHoriz() > getMaximumVert() ? - getMaximumHoriz():getMaximumVert(); - // calculate boundaries in degrees - double minLong = getUnscaledLongitude(-maxValue); - double maxLong = getUnscaledLongitude(maxValue); - double minLat = getUnscaledLatitude(-maxValue); - double maxLat = getUnscaledLatitude(maxValue); - // work out what line separation to use to give at least two lines - int sepIndex = -1; - double separation; - int numLatLines = 0, numLonLines = 0; - do - { - sepIndex++; - separation = COORD_SEPARATIONS[sepIndex]; - numLatLines = getNumLinesBetween(minLat, maxLat, separation); - numLonLines = getNumLinesBetween(minLong, maxLong, separation); - } - while ((numLonLines <= 1 || numLatLines <= 1) && sepIndex < MAX_COORD_SEPARATION_INDEX); - // create lines based on this separation - _latLinesDegs = getLines(minLat, maxLat, separation, numLatLines); - _lonLinesDegs = getLines(minLong, maxLong, separation, numLonLines); - // scale lines also - _latLinesScaled = new double[numLatLines]; - for (int i=0; i= inMin) numLines++; - value += inSeparation; - } - return numLines; - } - - - /** - * Get the line values in the given range using the specified separation - * @param inMin minimum value - * @param inMax maximum value - * @param inSeparation line separation - * @param inCount number of lines already counted - * @return array of line values - */ - private static double[] getLines(double inMin, double inMax, double inSeparation, int inCount) - { - double[] values = new double[inCount]; - // Start looking from round number of degrees below minimum - double value = (int) inMin; - if (inMin < 0.0) value = value - 1.0; - // Loop until bigger than maximum - int numLines = 0; - while (value < inMax) - { - if (value >= inMin) - { - values[numLines] = value; - numLines++; - } - value += inSeparation; - } - return values; - } - - /** - * @return array of latitude lines in degrees - */ - public double[] getLatitudeLines() - { - return _latLinesDegs; - } - /** - * @return array of longitude lines in degrees - */ - public double[] getLongitudeLines() - { - return _lonLinesDegs; - } - /** - * @return array of latitude lines in scaled units - */ - public double[] getScaledLatitudeLines() - { - return _latLinesScaled; - } - /** - * @return array of longitude lines in scaled units + * @return altitude range, in metres */ - public double[] getScaledLongitudeLines() + public double getAltitudeRange() { - return _lonLinesScaled; + return _altitudeRange; } } diff --git a/tim/prune/data/RangeStats.java b/tim/prune/data/RangeStats.java new file mode 100644 index 0000000..0ed8fc7 --- /dev/null +++ b/tim/prune/data/RangeStats.java @@ -0,0 +1,269 @@ +package tim.prune.data; + +import tim.prune.config.Config; + +/** + * Class to do calculations of range statistics such as distances, durations, + * speeds, gradients etc, and to hold the results of the calculations. + * Used by FullRangeDetails as well as the EstimateTime functions. + */ +public class RangeStats +{ + private boolean _valid = false; + private int _numPoints = 0; + private int _startIndex = 0, _endIndex = 0; + private int _numSegments = 0; + private AltitudeRange _totalAltitudeRange = null, _movingAltitudeRange = null; + private AltitudeRange _gentleAltitudeRange = null, _steepAltitudeRange = null; + private Timestamp _earliestTimestamp = null, _latestTimestamp = null; + private long _movingMilliseconds = 0L; + private boolean _timestampsIncomplete = false; + private double _totalDistanceRads = 0.0, _movingDistanceRads = 0.0; + // Note, maximum speed is not calculated here, use the SpeedData method instead + + private static final double STEEP_ANGLE = 0.15; // gradient steeper than 15% counts as steep + + + /** + * Constructor + * @param inTrack track to compile data for + * @param inStartIndex start index of range to examine + * @param inEndIndex end index (inclusive) of range to examine + */ + public RangeStats(Track inTrack, int inStartIndex, int inEndIndex) + { + if (inTrack != null && inStartIndex >= 0 && inEndIndex > inStartIndex + && inEndIndex < inTrack.getNumPoints()) + { + _valid = calculateStats(inTrack, inStartIndex, inEndIndex); + } + } + + /** + * Calculate the statistics and populate the member variables with the results + * @param inTrack track + * @param inStartIndex start index of range + * @param inEndIndex end index (inclusive) of range + * @return true on success + */ + private boolean calculateStats(Track inTrack, int inStartIndex, int inEndIndex) + { + _startIndex = inStartIndex; _endIndex = inEndIndex; + _numPoints = inEndIndex - inStartIndex + 1; + _totalAltitudeRange = new AltitudeRange(); + _movingAltitudeRange = new AltitudeRange(); + _gentleAltitudeRange = new AltitudeRange(); + _steepAltitudeRange = new AltitudeRange(); + DataPoint prevPoint = null; + Altitude prevAltitude = null; + _totalDistanceRads = _movingDistanceRads = 0.0; + double radsSinceLastAltitude = 0.0; + _movingMilliseconds = 0L; + + // Loop over the points in the range + for (int i=inStartIndex; i<= inEndIndex; i++) + { + DataPoint p = inTrack.getPoint(i); + if (p == null) return false; + // ignore all waypoints + if (p.isWaypoint()) continue; + + if (p.getSegmentStart()) {_numSegments++;} + // Get the distance to the previous track point + if (prevPoint != null) + { + double rads = DataPoint.calculateRadiansBetween(prevPoint, p); + _totalDistanceRads += rads; + if (!p.getSegmentStart()) { + _movingDistanceRads += rads; + } + // Keep track of rads since last point with an altitude + radsSinceLastAltitude += rads; + } + // Get the altitude difference to the previous track point + if (p.hasAltitude()) + { + Altitude altitude = p.getAltitude(); + _totalAltitudeRange.addValue(altitude); + if (p.getSegmentStart()) { + _movingAltitudeRange.ignoreValue(altitude); + } + else + { + _movingAltitudeRange.addValue(altitude); + if (prevAltitude != null) + { + // Work out gradient, see whether to ignore/add to gentle or steep + double heightDiff = altitude.getMetricValue() - prevAltitude.getMetricValue(); + double metricDist = Distance.convertRadiansToDistance(radsSinceLastAltitude, UnitSetLibrary.UNITS_METRES); + final boolean isSteep = metricDist < 0.001 || (Math.abs(heightDiff / metricDist) > STEEP_ANGLE); + if (isSteep) { + _steepAltitudeRange.ignoreValue(prevAltitude); + _steepAltitudeRange.addValue(altitude); + } + else { + _gentleAltitudeRange.ignoreValue(prevAltitude); + _gentleAltitudeRange.addValue(altitude); + } + } + } + prevAltitude = altitude; + radsSinceLastAltitude = 0.0; + } + + if (p.hasTimestamp()) + { + if (_earliestTimestamp == null || p.getTimestamp().isBefore(_earliestTimestamp)) { + _earliestTimestamp = p.getTimestamp(); + } + if (_latestTimestamp == null || p.getTimestamp().isAfter(_latestTimestamp)) { + _latestTimestamp = p.getTimestamp(); + } + // Work out duration without segment gaps + if (!p.getSegmentStart() && prevPoint != null && prevPoint.hasTimestamp()) + { + long millisLater = p.getTimestamp().getMillisecondsSince(prevPoint.getTimestamp()); + if (millisLater < 0) {_timestampsIncomplete = true;} + _movingMilliseconds += millisLater; + } + } + else if (!p.getSegmentStart()) { + _timestampsIncomplete = true; + } + + prevPoint = p; + } + return true; + } + + + /** @return true if results are valid */ + public boolean isValid() { + return _valid; + } + + /** @return start index of range */ + public int getStartIndex() { + return _startIndex; + } + + /** @return end index of range */ + public int getEndIndex() { + return _endIndex; + } + + /** @return number of points in range */ + public int getNumPoints() { + return _numPoints; + } + + /** @return number of segments in range */ + public int getNumSegments() { + return _numSegments; + } + + /** @return altitude range of range including segment gaps */ + public AltitudeRange getTotalAltitudeRange() { + return _totalAltitudeRange; + } + + /** @return altitude range of range just within segments */ + public AltitudeRange getMovingAltitudeRange() { + return _movingAltitudeRange; + } + + /** @return altitude range of range just considering low gradient bits */ + public AltitudeRange getGentleAltitudeRange() { + return _gentleAltitudeRange; + } + + /** @return altitude range of range just considering high gradient bits */ + public AltitudeRange getSteepAltitudeRange() { + return _steepAltitudeRange; + } + + /** @return the earliest timestamp found */ + public Timestamp getEarliestTimestamp() { + return _earliestTimestamp; + } + + /** @return the latest timestamp found */ + public Timestamp getLatestTimestamp() { + return _latestTimestamp; + } + + /** @return total number of seconds in the range */ + public long getTotalDurationInSeconds() + { + if (_earliestTimestamp != null && _latestTimestamp != null) { + return _latestTimestamp.getSecondsSince(_earliestTimestamp); + } + return 0L; + } + + /** @return number of seconds within the segments of the range */ + public long getMovingDurationInSeconds() + { + return _movingMilliseconds / 1000; + } + + /** @return true if any timestamps are missing or out of sequence */ + public boolean getTimestampsIncomplete() { + return _timestampsIncomplete; + } + + /** @return total distance in the current distance units (km or mi) */ + public double getTotalDistance() { + return Distance.convertRadiansToDistance(_totalDistanceRads); + } + + /** @return moving distance in the current distance units (km or mi) */ + public double getMovingDistance() { + return Distance.convertRadiansToDistance(_movingDistanceRads); + } + + /** @return moving distance in km */ + public double getMovingDistanceKilometres() { + return Distance.convertRadiansToDistance(_movingDistanceRads, UnitSetLibrary.UNITS_KILOMETRES); + } + + /** @return the total gradient in % (including segment gaps) */ + public double getTotalGradient() + { + double dist = Distance.convertRadiansToDistance(_totalDistanceRads, UnitSetLibrary.UNITS_METRES); + if (dist > 0.0 && _totalAltitudeRange.hasRange()) { + return _totalAltitudeRange.getMetricHeightDiff() / dist * 100.0; + } + return 0.0; + } + + /** @return the moving gradient in % (ignoring segment gaps) */ + public double getMovingGradient() + { + double dist = Distance.convertRadiansToDistance(_movingDistanceRads, UnitSetLibrary.UNITS_METRES); + if (dist > 0.0 && _movingAltitudeRange.hasRange()) { + return _movingAltitudeRange.getMetricHeightDiff() / dist * 100.0; + } + return 0.0; + } + + /** @return the total vertical speed (including segment gaps) in current vspeed units */ + public double getTotalVerticalSpeed() + { + long time = getTotalDurationInSeconds(); + if (time > 0 && _totalAltitudeRange.hasRange()) { + return _totalAltitudeRange.getMetricHeightDiff() / time * Config.getUnitSet().getVerticalSpeedUnit().getMultFactorFromStd(); + } + return 0.0; + } + + /** @return the moving vertical speed (ignoring segment gaps) in current vspeed units */ + public double getMovingVerticalSpeed() + { + long time = getMovingDurationInSeconds(); + if (time > 0 && _movingAltitudeRange.hasRange()) { + return _movingAltitudeRange.getMetricHeightDiff() / time * Config.getUnitSet().getVerticalSpeedUnit().getMultFactorFromStd(); + } + return 0.0; + } +} diff --git a/tim/prune/data/Selection.java b/tim/prune/data/Selection.java index f5c41ff..6125eac 100644 --- a/tim/prune/data/Selection.java +++ b/tim/prune/data/Selection.java @@ -19,7 +19,6 @@ public class Selection private AltitudeRange _altitudeRange = null; private long _totalSeconds = 0L, _movingSeconds = 0L; private double _angDistance = -1.0, _angMovingDistance = -1.0; - private int _numSegments = 0; /** @@ -64,7 +63,6 @@ public class Selection */ private void recalculate() { - _numSegments = 0; final int numPoints = _track.getNumPoints(); // Recheck if the number of points has changed if (numPoints != _prevNumPoints) { @@ -109,16 +107,11 @@ public class Selection { double radians = DataPoint.calculateRadiansBetween(lastPoint, currPoint); _angDistance += radians; - if (currPoint.getSegmentStart()) { - _numSegments++; - } - else { + if (!currPoint.getSegmentStart()) { _angMovingDistance += radians; } } lastPoint = currPoint; - // If it's a track point then there must be at least one segment - if (_numSegments == 0) {_numSegments = 1;} } } if (endTime != null) { @@ -192,14 +185,6 @@ public class Selection return Distance.convertRadiansToDistance(_angMovingDistance); } - /** - * @return number of segments in selection - */ - public int getNumSegments() - { - return _numSegments; - } - /** * Clear selected point, range, photo and audio */ diff --git a/tim/prune/data/Speed.java b/tim/prune/data/Speed.java new file mode 100644 index 0000000..80cb7df --- /dev/null +++ b/tim/prune/data/Speed.java @@ -0,0 +1,102 @@ +package tim.prune.data; + +/** + * Class to hold either a horizontal speed or a vertical speed + * including the units + */ +public class Speed +{ + private double _value = 0.0; + private Unit _unit = null; + private boolean _valid = false; + + /** + * Constructor + * @param inValue value + * @param inUnit unit, such as m/s or km/h + */ + public Speed(double inValue, Unit inUnit) + { + _value = inValue; + _unit = inUnit; + _valid = isValidUnit(inUnit); + } + + /** + * Constructor + * @param inValue value as string + * @param inUnit unit, such as m/s or km/h + */ + public Speed(String inValue, Unit inUnit) + { + try { + _value = Double.parseDouble(inValue); + _unit = inUnit; + _valid = isValidUnit(inUnit); + } + catch (Exception e) + { + _valid = false; + } + } + + /** + * Check if the given unit is valid for a speed + * @param inUnit unit + * @return true if it's valid + */ + private static boolean isValidUnit(Unit inUnit) + { + return inUnit != null && (inUnit == UnitSetLibrary.SPEED_UNITS_METRESPERSEC + || inUnit == UnitSetLibrary.SPEED_UNITS_KMPERHOUR + || inUnit == UnitSetLibrary.SPEED_UNITS_FEETPERSEC + || inUnit == UnitSetLibrary.SPEED_UNITS_MILESPERHOUR + || inUnit == UnitSetLibrary.SPEED_UNITS_KNOTS); + } + + /** + * Invert the speed value, for example when vertical speeds are positive downwards + */ + public void invert() { + if (_valid) _value = -_value; + } + + /** @return the numerical value in whatever units they're in */ + public double getValue() {return _value;} + + /** @return the units they're in */ + public Unit getUnit() {return _unit;} + + /** + * @return speed value in metres per second + */ + public double getValueInMetresPerSec() + { + if (!_valid) return 0.0; + return _value / _unit.getMultFactorFromStd(); + } + + /** + * @param inUnit specified speed units + * @return speed value in the specified units + */ + public double getValue(Unit inUnit) + { + if (!_valid || !isValidUnit(inUnit)) return 0.0; + return getValueInMetresPerSec() * inUnit.getMultFactorFromStd(); + } + + /** @return true if this is valid */ + public boolean isValid() {return _valid;} + + /** + * Copy the values from the other object into this one, to make this one a clone + * @param inOther other speed object + */ + public void copyFrom(Speed inOther) + { + _value = inOther._value; + _unit = inOther._unit; + _valid = inOther._valid; + } +} diff --git a/tim/prune/data/SpeedCalculator.java b/tim/prune/data/SpeedCalculator.java index d83a7bd..f6f429c 100644 --- a/tim/prune/data/SpeedCalculator.java +++ b/tim/prune/data/SpeedCalculator.java @@ -9,7 +9,7 @@ import tim.prune.config.Config; public abstract class SpeedCalculator { /** - * Calculate the speed value of the track at the specified index + * Calculate the horizontal speed value of the track at the specified index * @param inTrack track object * @param inIndex index of point to calculate speed for * @param inValue object in which to place result of calculation @@ -28,13 +28,10 @@ public abstract class SpeedCalculator double speedValue = 0.0; // First, see if point has a speed value already - // FIXME: How do we know what units this speed is in? m/s or mph or km/h or what? - String speedStr = point.getFieldValue(Field.SPEED); - try { - speedValue = Double.parseDouble(speedStr); + if (point.hasHSpeed()) { + speedValue = point.getHSpeed().getValue(Config.getUnitSet().getSpeedUnit()); pointHasSpeed = true; } - catch (Exception e) {} // ignore, leave pointHasSpeed false // otherwise, see if we can calculate it from the timestamps if (!pointHasSpeed && point.hasTimestamp() && !point.isWaypoint()) @@ -127,15 +124,10 @@ public abstract class SpeedCalculator double speedValue = 0.0; // First, see if point has a speed value already - if (point != null) + if (point != null && point.hasVSpeed()) { - // FIXME: Can we assume m/s or ft/s? - String speedStr = point.getFieldValue(Field.VERTICAL_SPEED); - try { - speedValue = Double.parseDouble(speedStr); - pointHasSpeed = true; - } - catch (Exception e) {} // ignore, leave pointHasSpeed false + speedValue = point.getVSpeed().getValue(Config.getUnitSet().getVerticalSpeedUnit()); + pointHasSpeed = true; } // otherwise, see if we can calculate it from the heights and timestamps if (!pointHasSpeed diff --git a/tim/prune/data/Timestamp.java b/tim/prune/data/Timestamp.java index 477a722..623d804 100644 --- a/tim/prune/data/Timestamp.java +++ b/tim/prune/data/Timestamp.java @@ -1,10 +1,11 @@ package tim.prune.data; import java.text.DateFormat; -import java.text.ParseException; +import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; +import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,7 +27,8 @@ public class Timestamp private static DateFormat[] ALL_DATE_FORMATS = null; private static Calendar CALENDAR = null; private static final Pattern ISO8601_FRACTIONAL_PATTERN - = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})\\.(\\d{1,3})Z?"); + = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(?:[\\.,](\\d{1,3}))?(Z|[\\+-]\\d{2}(?::?\\d{2})?)?"); + // year month day T hour minute sec millisec Z or +/- hours : minutes private static final Pattern GENERAL_TIMESTAMP_PATTERN = Pattern.compile("(\\d{4})\\D(\\d{2})\\D(\\d{2})\\D(\\d{2})\\D(\\d{2})\\D(\\d{2})"); private static long SECS_SINCE_1970 = 0L; @@ -68,12 +70,17 @@ public class Timestamp static { CALENDAR = Calendar.getInstance(); + TimeZone gmtZone = TimeZone.getTimeZone("GMT"); + CALENDAR.setTimeZone(gmtZone); MSECS_SINCE_1970 = CALENDAR.getTimeInMillis(); SECS_SINCE_1970 = MSECS_SINCE_1970 / 1000L; SECS_SINCE_GARTRIP = SECS_SINCE_1970 - GARTRIP_OFFSET; CALENDAR.add(Calendar.YEAR, -20); MSECS_SINCE_1990 = CALENDAR.getTimeInMillis(); TWENTY_YEARS_IN_SECS = (MSECS_SINCE_1970 - MSECS_SINCE_1990) / 1000L; + // Set timezone for output + ISO_8601_FORMAT.setTimeZone(gmtZone); + DEFAULT_DATE_FORMAT.setTimeZone(gmtZone); // Date formats ALL_DATE_FORMATS = new DateFormat[] { DEFAULT_DATE_FORMAT, @@ -145,7 +152,8 @@ public class Timestamp Integer.parseInt(fmatcher.group(4)), // hour Integer.parseInt(fmatcher.group(5)), // minute Integer.parseInt(fmatcher.group(6)), // second - fmatcher.group(7)); // fractional seconds + fmatcher.group(7), // fractional seconds + fmatcher.group(8)); // timezone, if any return true; } catch (NumberFormatException nfe) {} @@ -173,7 +181,7 @@ public class Timestamp Integer.parseInt(matcher.group(4)), Integer.parseInt(matcher.group(5)), Integer.parseInt(matcher.group(6)), - null); // no fractions of a second + null, null); // no fractions of a second and no timezone return true; } catch (NumberFormatException nfe2) {} // parse shouldn't fail if matcher matched @@ -193,14 +201,16 @@ public class Timestamp */ private boolean parseString(String inString, DateFormat inDateFormat) { - try + inDateFormat.setLenient(false); + ParsePosition pPos = new ParsePosition(0); + Date date = inDateFormat.parse(inString, pPos); + if (date != null && inString.length() == pPos.getIndex()) // require use of _all_ the string, not just the beginning { - Date date = inDateFormat.parse(inString); CALENDAR.setTime(date); _milliseconds = CALENDAR.getTimeInMillis(); return true; } - catch (ParseException e) {} + return false; } @@ -216,7 +226,7 @@ public class Timestamp */ public Timestamp(int inYear, int inMonth, int inDay, int inHour, int inMinute, int inSecond) { - _milliseconds = getMilliseconds(inYear, inMonth, inDay, inHour, inMinute, inSecond, null); + _milliseconds = getMilliseconds(inYear, inMonth, inDay, inHour, inMinute, inSecond, null, null); _valid = true; } @@ -241,12 +251,22 @@ public class Timestamp * @param inMinute minute * @param inSecond seconds * @param inFraction fractions of a second + * @param inTimezone timezone, if any * @return number of milliseconds */ private static long getMilliseconds(int inYear, int inMonth, int inDay, - int inHour, int inMinute, int inSecond, String inFraction) + int inHour, int inMinute, int inSecond, String inFraction, String inTimezone) { Calendar cal = Calendar.getInstance(); + // Timezone, if any + if (inTimezone == null || inTimezone.equals("") || inTimezone.equals("Z")) { + // No timezone, use zulu + cal.setTimeZone(TimeZone.getTimeZone("GMT")); + } + else { + // Timezone specified, pass to calendar + cal.setTimeZone(TimeZone.getTimeZone("GMT" + inTimezone)); + } cal.set(Calendar.YEAR, inYear); cal.set(Calendar.MONTH, inMonth - 1); cal.set(Calendar.DAY_OF_MONTH, inDay); @@ -426,7 +446,7 @@ public class Timestamp return format(ISO_8601_FORMAT); } if (_text == null) { - _text = (_valid?format(DEFAULT_DATE_FORMAT):""); + _text = format(DEFAULT_DATE_FORMAT); } return _text; } @@ -453,6 +473,7 @@ public class Timestamp */ private String format(DateFormat inFormat) { + CALENDAR.setTimeZone(TimeZone.getTimeZone("GMT")); CALENDAR.setTimeInMillis(_milliseconds); return inFormat.format(CALENDAR.getTime()); } @@ -463,6 +484,7 @@ public class Timestamp public Calendar getCalendar() { Calendar cal = Calendar.getInstance(); + cal.setTimeZone(TimeZone.getTimeZone("GMT")); cal.setTimeInMillis(_milliseconds); return cal; } diff --git a/tim/prune/data/Track.java b/tim/prune/data/Track.java index b28fd36..8623c58 100644 --- a/tim/prune/data/Track.java +++ b/tim/prune/data/Track.java @@ -62,9 +62,9 @@ public class Track * Load method, for initialising and reinitialising data * @param inFieldArray array of Field objects describing fields * @param inPointArray 2d object array containing data - * @param inAltFormat altitude format + * @param inOptions load options such as units */ - public void load(Field[] inFieldArray, Object[][] inPointArray, Altitude.Format inAltFormat) + public void load(Field[] inFieldArray, Object[][] inPointArray, PointCreateOptions inOptions) { if (inFieldArray == null || inPointArray == null) { @@ -81,7 +81,7 @@ public class Track { dataArray = (String[]) inPointArray[p]; // Convert to DataPoint objects - DataPoint point = new DataPoint(dataArray, _masterFieldList, inAltFormat); + DataPoint point = new DataPoint(dataArray, _masterFieldList, inOptions); if (point.isValid()) { _dataPoints[pointIndex] = point; @@ -333,12 +333,12 @@ public class Track * @param inStart start of range * @param inEnd end of range * @param inOffset offset to add (-ve to subtract) - * @param inFormat altitude format of offset + * @param inUnit altitude unit of offset * @param inDecimals number of decimal places in offset * @return true on success */ public boolean addAltitudeOffset(int inStart, int inEnd, double inOffset, - Altitude.Format inFormat, int inDecimals) + Unit inUnit, int inDecimals) { // sanity check if (inStart < 0 || inEnd < 0 || inStart >= inEnd || inEnd >= _numPoints) { @@ -353,7 +353,7 @@ public class Track { // This point has an altitude so add the offset to it foundAlt = true; - alt.addOffset(inOffset, inFormat, inDecimals); + alt.addOffset(inOffset, inUnit, inDecimals); _dataPoints[i].setModified(false); } } @@ -578,18 +578,19 @@ public class Track double latitudeDiff = 0.0, longitudeDiff = 0.0; double totalAltitude = 0; int numAltitudes = 0; - Altitude.Format altFormat = Altitude.Format.NO_FORMAT; + Unit altUnit = null; // loop between start and end points for (int i=inStartIndex; i<= inEndIndex; i++) { DataPoint currPoint = getPoint(i); latitudeDiff += (currPoint.getLatitude().getDouble() - firstLatitude); longitudeDiff += (currPoint.getLongitude().getDouble() - firstLongitude); - if (currPoint.hasAltitude()) { - totalAltitude += currPoint.getAltitude().getValue(altFormat); + if (currPoint.hasAltitude()) + { + totalAltitude += currPoint.getAltitude().getValue(altUnit); // Use altitude format of first valid altitude - if (altFormat == Altitude.Format.NO_FORMAT) - altFormat = currPoint.getAltitude().getFormat(); + if (altUnit == null) + altUnit = currPoint.getAltitude().getUnit(); numAltitudes++; } } @@ -597,7 +598,9 @@ public class Track double meanLatitude = firstLatitude + (latitudeDiff / numPoints); double meanLongitude = firstLongitude + (longitudeDiff / numPoints); Altitude meanAltitude = null; - if (numAltitudes > 0) {meanAltitude = new Altitude((int) (totalAltitude / numAltitudes), altFormat);} + if (numAltitudes > 0) { + meanAltitude = new Altitude((int) (totalAltitude / numAltitudes), altUnit); + } DataPoint insertedPoint = new DataPoint(new Latitude(meanLatitude, Coordinate.FORMAT_NONE), new Longitude(meanLongitude, Coordinate.FORMAT_NONE), meanAltitude); @@ -657,7 +660,7 @@ public class Track */ public DoubleRange getXRange() { - if (!_scaled) scalePoints(); + if (!_scaled) {scalePoints();} return _xRange; } @@ -666,7 +669,7 @@ public class Track */ public DoubleRange getYRange() { - if (!_scaled) scalePoints(); + if (!_scaled) {scalePoints();} return _yRange; } @@ -675,7 +678,7 @@ public class Track */ public DoubleRange getLatRange() { - if (!_scaled) scalePoints(); + if (!_scaled) {scalePoints();} return _latRange; } /** @@ -683,7 +686,7 @@ public class Track */ public DoubleRange getLonRange() { - if (!_scaled) scalePoints(); + if (!_scaled) {scalePoints();} return _longRange; } @@ -693,7 +696,7 @@ public class Track */ public double getX(int inPointNum) { - if (!_scaled) scalePoints(); + if (!_scaled) {scalePoints();} return _xValues[inPointNum]; } @@ -703,7 +706,7 @@ public class Track */ public double getY(int inPointNum) { - if (!_scaled) scalePoints(); + if (!_scaled) {scalePoints();} return _yValues[inPointNum]; } @@ -770,7 +773,7 @@ public class Track */ public boolean hasTrackPoints() { - if (!_scaled) scalePoints(); + if (!_scaled) {scalePoints();} return _hasTrackpoint; } @@ -779,7 +782,7 @@ public class Track */ public boolean hasWaypoints() { - if (!_scaled) scalePoints(); + if (!_scaled) {scalePoints();} return _hasWaypoint; } @@ -862,7 +865,7 @@ public class Track * Scale all the points in the track to gain x and y values * ready for plotting */ - private void scalePoints() + private synchronized void scalePoints() { // Loop through all points in track, to see limits of lat, long _longRange = new DoubleRange(); diff --git a/tim/prune/data/TrackExtents.java b/tim/prune/data/TrackExtents.java new file mode 100644 index 0000000..0bf3081 --- /dev/null +++ b/tim/prune/data/TrackExtents.java @@ -0,0 +1,103 @@ +package tim.prune.data; + +/** + * Class to hold the extents of a track, in 2d and in 3d, + * and to calculate a square area with a default border + */ +public class TrackExtents +{ + /** Track object */ + private Track _track = null; + /** X and Y ranges */ + private DoubleRange _xRange = null, _yRange = null; + + /** Border multiplier */ + private static final double BORDER_MULTIPLIER = 1.1; // 10% border + + /** + * Constructor + * @param inTrack track object to take extents from + */ + public TrackExtents(Track inTrack) + { + _track = inTrack; + _xRange = inTrack.getXRange().copy(); + _yRange = inTrack.getYRange().copy(); + } + + + /** + * Make the x and y ranges square with a default border around + */ + public void applySquareBorder() + { + // Find the middle of the x and y + final double midXvalue = _xRange.getMidValue(); + final double midYvalue = _yRange.getMidValue(); + // Find x and y range, take maximum + double xyRange = Math.max(_xRange.getRange(), _yRange.getRange()) * BORDER_MULTIPLIER; + if (getHorizontalDistanceMetres() < 10.0) + { + // all the points are near enough on the same spot, expand scale to avoid dividing by zero + xyRange = 0.1; + } + + // Apply these new min and max to the ranges + _xRange.addValue(midXvalue - xyRange / 2.0); + _xRange.addValue(midXvalue + xyRange / 2.0); + _yRange.addValue(midYvalue - xyRange / 2.0); + _yRange.addValue(midYvalue + xyRange / 2.0); + } + + /** @return x range */ + public DoubleRange getXRange() { + return _xRange; + } + + /** @return y range */ + public DoubleRange getYRange() { + return _yRange; + } + + /** @return altitude range */ + public DoubleRange getAltitudeRange() + { + final int numPoints = _track.getNumPoints(); + DoubleRange altRange = new DoubleRange(); + for (int i=0; i").append(I18nManager.getText("dialog.about.languages")).append(" : ") .append("\u010de\u0161tina, deutsch, english, espa\u00F1ol, fran\u00E7ais, italiano, magyar,
" + " nederlands, polski, portugu\u00EAs, \u0440\u0443\u0441\u0441\u043a\u0438\u0439 (russian), \u4e2d\u6587 (chinese), \u65E5\u672C\u8A9E (japanese),
" + - " \uD55C\uAD6D\uC5B4/\uC870\uC120\uB9D0 (korean), schwiizerd\u00FC\u00FCtsch, t\u00FCrk\u00E7e, rom\u00E2n\u0103, afrikaans, bahasa indonesia

"); + " \uD55C\uAD6D\uC5B4/\uC870\uC120\uB9D0 (korean), schwiizerd\u00FC\u00FCtsch, t\u00FCrk\u00E7e, afrikaans, rom\u00E2n\u0103

"); descBuffer.append("

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

"); JEditorPane descPane = new JEditorPane("text/html", descBuffer.toString()); descPane.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); diff --git a/tim/prune/function/AddAltitudeOffset.java b/tim/prune/function/AddAltitudeOffset.java index 85c52de..c98ab3e 100644 --- a/tim/prune/function/AddAltitudeOffset.java +++ b/tim/prune/function/AddAltitudeOffset.java @@ -19,8 +19,9 @@ import tim.prune.App; import tim.prune.GenericFunction; import tim.prune.I18nManager; import tim.prune.config.Config; -import tim.prune.data.Altitude; import tim.prune.data.Field; +import tim.prune.data.Unit; +import tim.prune.data.UnitSetLibrary; /** * Class to provide the function to add an altitude offset to a track range @@ -31,7 +32,7 @@ public class AddAltitudeOffset extends GenericFunction private JLabel _descLabel = null; private JTextField _editField = null; private JButton _okButton = null; - private Altitude.Format _altFormat = Altitude.Format.NO_FORMAT; + private Unit _altUnit = null; /** @@ -152,11 +153,11 @@ public class AddAltitudeOffset extends GenericFunction */ private void setLabelText() { - _altFormat = Altitude.Format.FEET; + _altUnit = UnitSetLibrary.UNITS_FEET; if (Config.getUnitSet().getAltitudeUnit().isStandard()) { - _altFormat = Altitude.Format.METRES; + _altUnit = UnitSetLibrary.UNITS_METRES; } - final String unitKey = (_altFormat==Altitude.Format.METRES?"units.metres.short":"units.feet.short"); + final String unitKey = _altUnit.getShortnameKey(); _descLabel.setText(I18nManager.getText("dialog.addaltitude.desc") + " (" + I18nManager.getText(unitKey) + ")"); } @@ -166,7 +167,7 @@ public class AddAltitudeOffset extends GenericFunction private void finish() { // Pass information back to App to complete function - _app.finishAddAltitudeOffset(_editField.getText(), _altFormat); + _app.finishAddAltitudeOffset(_editField.getText(), _altUnit); _dialog.dispose(); } } diff --git a/tim/prune/function/AddMapSourceDialog.java b/tim/prune/function/AddMapSourceDialog.java index da1f15e..52a8874 100644 --- a/tim/prune/function/AddMapSourceDialog.java +++ b/tim/prune/function/AddMapSourceDialog.java @@ -105,9 +105,21 @@ public class AddMapSourceDialog KeyAdapter keyListener = new KeyAdapter() { public void keyReleased(KeyEvent e) { super.keyReleased(e); + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + _addDialog.dispose(); + } + else { + enableOK(); + } + } + }; + // Listener for any gui changes (to enable ok when anything changes on an edit) + ActionListener okEnabler = new ActionListener() { + public void actionPerformed(ActionEvent arg0) { enableOK(); } }; + // openstreetmap panel JPanel osmPanel = new JPanel(); osmPanel.setLayout(new BorderLayout()); @@ -144,6 +156,8 @@ public class AddMapSourceDialog radioGroup.add(_baseTypeRadios[i]); c.gridx = 2+i; c.weightx = 0.0; gbPanel.add(_baseTypeRadios[i], c); + // Each type radio needs listener to call enableOk() + _baseTypeRadios[i].addActionListener(okEnabler); } // Top layer @@ -161,6 +175,8 @@ public class AddMapSourceDialog radioGroup.add(_topTypeRadios[i]); c.gridx = 2+i; c.weightx = 0.0; gbPanel.add(_topTypeRadios[i], c); + // Each type radio needs listener to call enableOk() + _topTypeRadios[i].addActionListener(okEnabler); } // Max zoom c.gridx = 0; c.gridy = 3; @@ -169,6 +185,8 @@ public class AddMapSourceDialog for (int i=10; i<=20; i++) { _oZoomCombo.addItem("" + i); } + // zoom dropdown needs listener to call enableOk() + _oZoomCombo.addActionListener(okEnabler); c.gridx = 1; gbPanel.add(_oZoomCombo, c); osmPanel.add(gbPanel, BorderLayout.NORTH); diff --git a/tim/prune/function/DownloadOsmFunction.java b/tim/prune/function/DownloadOsmFunction.java index e3d3627..76c69da 100644 --- a/tim/prune/function/DownloadOsmFunction.java +++ b/tim/prune/function/DownloadOsmFunction.java @@ -243,7 +243,7 @@ public class DownloadOsmFunction extends GenericFunction implements Runnable */ public void run() { - final String url = "http://xapi.openstreetmap.org/api/0.6/map?bbox=" + + final String url = "http://www.overpass-api.de/api/xapi?map?bbox=" + _latLonLabels[1].getText() + "," + _latLonLabels[3].getText() + "," + _latLonLabels[2].getText() + "," + _latLonLabels[0].getText(); diff --git a/tim/prune/function/Export3dFunction.java b/tim/prune/function/Export3dFunction.java index 5b12125..9e92e62 100644 --- a/tim/prune/function/Export3dFunction.java +++ b/tim/prune/function/Export3dFunction.java @@ -9,7 +9,7 @@ import tim.prune.GenericFunction; public abstract class Export3dFunction extends GenericFunction { /** altitude exaggeration factor */ - protected double _altFactor = 50.0; + protected double _altFactor = 5.0; /** * Required constructor diff --git a/tim/prune/function/FullRangeDetails.java b/tim/prune/function/FullRangeDetails.java index a0bae65..d9dbb70 100644 --- a/tim/prune/function/FullRangeDetails.java +++ b/tim/prune/function/FullRangeDetails.java @@ -8,7 +8,6 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; -import java.text.NumberFormat; import javax.swing.BorderFactory; import javax.swing.JButton; @@ -20,9 +19,7 @@ import tim.prune.App; import tim.prune.GenericFunction; import tim.prune.I18nManager; import tim.prune.config.Config; -import tim.prune.data.Altitude; -import tim.prune.data.AltitudeRange; -import tim.prune.data.DataPoint; +import tim.prune.data.RangeStats; import tim.prune.data.Selection; import tim.prune.data.Unit; import tim.prune.gui.DisplayUtils; @@ -63,10 +60,6 @@ public class FullRangeDetails extends GenericFunction /** Labels for vertical speed */ private JLabel _totalVertSpeedLabel, _movingVertSpeedLabel = null; - /** Number formatter for one decimal place */ - private static final NumberFormat FORMAT_ONE_DP = NumberFormat.getNumberInstance(); - /** Flexible number formatter for different decimal places */ - private NumberFormat _distanceFormatter = NumberFormat.getInstance(); /** * Constructor @@ -75,8 +68,6 @@ public class FullRangeDetails extends GenericFunction public FullRangeDetails(App inApp) { super(inApp); - FORMAT_ONE_DP.setMaximumFractionDigits(1); - FORMAT_ONE_DP.setMinimumFractionDigits(1); } /** Get the name key */ @@ -250,11 +241,14 @@ public class FullRangeDetails extends GenericFunction private void updateDetails() { Selection selection = _app.getTrackInfo().getSelection(); + // Do the calculations with a separate class + RangeStats stats = new RangeStats(_app.getTrackInfo().getTrack(), selection.getStart(), selection.getEnd()); + // Number of points - _numPointsLabel.setText("" + (selection.getEnd()-selection.getStart()+1)); + _numPointsLabel.setText("" + stats.getNumPoints()); // Number of segments - _numSegsLabel.setText("" + selection.getNumSegments()); - final boolean isMultiSegments = (selection.getNumSegments() > 1); + _numSegsLabel.setText("" + stats.getNumSegments()); + final boolean isMultiSegments = (stats.getNumSegments() > 1); // Set visibility of third column accordingly _movingDistanceLabel.setVisible(isMultiSegments); _movingDurationLabel.setVisible(isMultiSegments); @@ -265,132 +259,78 @@ public class FullRangeDetails extends GenericFunction _movingGradientLabel.setVisible(isMultiSegments); _movingVertSpeedLabel.setVisible(isMultiSegments); - // Distance in current units + // Total and moving distance in current units final Unit distUnit = Config.getUnitSet().getDistanceUnit(); final String distUnitsStr = I18nManager.getText(distUnit.getShortnameKey()); - final double selectionDistance = selection.getDistance(); - _totalDistanceLabel.setText(roundedNumber(selectionDistance) + " " + distUnitsStr); + _totalDistanceLabel.setText(DisplayUtils.roundedNumber(stats.getTotalDistance()) + " " + distUnitsStr); + _movingDistanceLabel.setText(DisplayUtils.roundedNumber(stats.getMovingDistance()) + " " + distUnitsStr); // Duration - long numSecs = selection.getNumSeconds(); - _totalDurationLabel.setText(DisplayUtils.buildDurationString(numSecs)); + _totalDurationLabel.setText(DisplayUtils.buildDurationString(stats.getTotalDurationInSeconds())); + _movingDurationLabel.setText(DisplayUtils.buildDurationString(stats.getMovingDurationInSeconds())); + // Climb and descent final Unit altUnit = Config.getUnitSet().getAltitudeUnit(); final String altUnitsStr = " " + I18nManager.getText(altUnit.getShortnameKey()); - if (selection.getAltitudeRange().hasRange()) { - _totalClimbLabel.setText(selection.getAltitudeRange().getClimb(altUnit) + altUnitsStr); - _totalDescentLabel.setText(selection.getAltitudeRange().getDescent(altUnit) + altUnitsStr); + if (stats.getTotalAltitudeRange().hasRange()) { + _totalClimbLabel.setText(stats.getTotalAltitudeRange().getClimb(altUnit) + altUnitsStr); + _totalDescentLabel.setText(stats.getTotalAltitudeRange().getDescent(altUnit) + altUnitsStr); } else { _totalClimbLabel.setText(""); _totalDescentLabel.setText(""); } + if (stats.getMovingAltitudeRange().hasRange()) { + _movingClimbLabel.setText(stats.getMovingAltitudeRange().getClimb(altUnit) + altUnitsStr); + _movingDescentLabel.setText(stats.getMovingAltitudeRange().getDescent(altUnit) + altUnitsStr); + } + else { + _movingClimbLabel.setText(""); + _movingDescentLabel.setText(""); + } // Overall pace and speed final String speedUnitsStr = I18nManager.getText(Config.getUnitSet().getSpeedUnit().getShortnameKey()); - if (numSecs > 0 && selectionDistance > 0) + long numSecs = stats.getTotalDurationInSeconds(); + double dist = stats.getTotalDistance(); + if (numSecs > 0 && dist > 0) { - _totalPaceLabel.setText( - DisplayUtils.buildDurationString((long) (numSecs/selectionDistance)) + _totalSpeedLabel.setText(DisplayUtils.roundedNumber(dist/numSecs*3600.0) + " " + speedUnitsStr); + _totalPaceLabel.setText(DisplayUtils.buildDurationString((long) (numSecs/dist)) + " / " + distUnitsStr); - _totalSpeedLabel.setText(roundedNumber(selectionDistance/numSecs*3600.0) - + " " + speedUnitsStr); } else { - _totalPaceLabel.setText(""); _totalSpeedLabel.setText(""); + _totalPaceLabel.setText(""); } - - // Moving distance - double movingDist = selection.getMovingDistance(); - _movingDistanceLabel.setText(roundedNumber(movingDist) + " " + distUnitsStr); - // Moving average speed - long numMovingSecs = selection.getMovingSeconds(); - if (numMovingSecs > 0) + // and same for within the segments + numSecs = stats.getMovingDurationInSeconds(); + dist = stats.getMovingDistance(); + if (numSecs > 0 && dist > 0) { - _movingDurationLabel.setText(DisplayUtils.buildDurationString(numMovingSecs)); - _movingSpeedLabel.setText(roundedNumber(movingDist/numMovingSecs*3600.0) - + " " + speedUnitsStr); - _movingPaceLabel.setText( - DisplayUtils.buildDurationString((long) (numMovingSecs/movingDist)) + _movingSpeedLabel.setText(DisplayUtils.roundedNumber(dist/numSecs*3600.0) + " " + speedUnitsStr); + _movingPaceLabel.setText(DisplayUtils.buildDurationString((long) (numSecs/dist)) + " / " + distUnitsStr); } - else - { - _movingDurationLabel.setText(""); + else { _movingSpeedLabel.setText(""); _movingPaceLabel.setText(""); } - // Moving gradient and moving climb/descent - Altitude firstAlt = null, lastAlt = null; - Altitude veryFirstAlt = null, veryLastAlt = null; - AltitudeRange altRange = new AltitudeRange(); - double movingHeightDiff = 0.0; - if (movingDist > 0.0) - { - for (int pNum = selection.getStart(); pNum <= selection.getEnd(); pNum++) - { - DataPoint p = _app.getTrackInfo().getTrack().getPoint(pNum); - if (p != null && !p.isWaypoint()) - { - // If we're starting a new segment, calculate the height diff of the previous one - if (p.getSegmentStart()) - { - if (firstAlt != null && firstAlt.isValid() && lastAlt != null && lastAlt.isValid()) - movingHeightDiff = movingHeightDiff + lastAlt.getMetricValue() - firstAlt.getMetricValue(); - firstAlt = null; lastAlt = null; - } - Altitude alt = p.getAltitude(); - if (alt != null && alt.isValid()) - { - if (firstAlt == null) firstAlt = alt; - else lastAlt = alt; - if (veryFirstAlt == null) veryFirstAlt = alt; - else veryLastAlt = alt; - } - // Keep track of climb and descent too - if (p.getSegmentStart()) - altRange.ignoreValue(alt); - else - altRange.addValue(alt); - } - } - // deal with last segment - if (firstAlt != null && firstAlt.isValid() && lastAlt != null && lastAlt.isValid()) - movingHeightDiff = movingHeightDiff + lastAlt.getMetricValue() - firstAlt.getMetricValue(); - final double metricMovingDist = movingDist / distUnit.getMultFactorFromStd(); // convert back to metres - final double gradient = movingHeightDiff * 100.0 / metricMovingDist; - _movingGradientLabel.setText(FORMAT_ONE_DP.format(gradient) + " %"); - } - if (!altRange.hasRange()) { - _movingGradientLabel.setText(""); - } - final boolean hasAltitudes = veryFirstAlt != null && veryFirstAlt.isValid() && veryLastAlt != null && veryLastAlt.isValid(); - - // Total gradient - final double metreDist = selection.getDistance() / distUnit.getMultFactorFromStd(); // convert back to metres - if (hasAltitudes && metreDist > 0.0) - { - // got an altitude and range - int altDiffInMetres = veryLastAlt.getValue(Altitude.Format.METRES) - veryFirstAlt.getValue(Altitude.Format.METRES); - double gradient = altDiffInMetres * 100.0 / metreDist; - _totalGradientLabel.setText(FORMAT_ONE_DP.format(gradient) + " %"); + // Gradient + if (stats.getTotalAltitudeRange().hasRange()) { + _totalGradientLabel.setText(DisplayUtils.formatOneDp(stats.getTotalGradient()) + " %"); } else { - // no altitude given _totalGradientLabel.setText(""); } - - // Moving climb/descent - if (altRange.hasRange()) { - _movingClimbLabel.setText(altRange.getClimb(altUnit) + altUnitsStr); - _movingDescentLabel.setText(altRange.getDescent(altUnit) + altUnitsStr); + if (stats.getMovingAltitudeRange().hasRange()) { + _movingGradientLabel.setText(DisplayUtils.formatOneDp(stats.getMovingGradient()) + " %"); } else { - _movingClimbLabel.setText(""); - _movingDescentLabel.setText(""); + _movingGradientLabel.setText(""); } + // Maximum speed SpeedData speeds = new SpeedData(_app.getTrackInfo().getTrack()); speeds.init(Config.getUnitSet()); @@ -402,7 +342,7 @@ public class FullRangeDetails extends GenericFunction } } if (maxSpeed > 0.0) { - _maxSpeedLabel.setText(roundedNumber(maxSpeed) + " " + speedUnitsStr); + _maxSpeedLabel.setText(DisplayUtils.roundedNumber(maxSpeed) + " " + speedUnitsStr); } else { _maxSpeedLabel.setText(""); @@ -410,40 +350,17 @@ public class FullRangeDetails extends GenericFunction // vertical speed final String vertSpeedUnitsStr = I18nManager.getText(Config.getUnitSet().getVerticalSpeedUnit().getShortnameKey()); - if (hasAltitudes && metreDist > 0.0 && numSecs > 0) + if (stats.getMovingAltitudeRange().hasRange() && stats.getTotalDurationInSeconds() > 0) { - // got an altitude and time - do total - final int altDiffInMetres = veryLastAlt.getValue(Altitude.Format.METRES) - veryFirstAlt.getValue(Altitude.Format.METRES); - final double altDiff = altDiffInMetres * altUnit.getMultFactorFromStd(); - _totalVertSpeedLabel.setText(roundedNumber(altDiff/numSecs) + " " + vertSpeedUnitsStr); - // and moving - _movingVertSpeedLabel.setText(roundedNumber(movingHeightDiff * altUnit.getMultFactorFromStd() / numMovingSecs) + " " + vertSpeedUnitsStr); + // got an altitude and time - do totals + _totalVertSpeedLabel.setText(DisplayUtils.roundedNumber(stats.getTotalVerticalSpeed()) + " " + vertSpeedUnitsStr); + _movingVertSpeedLabel.setText(DisplayUtils.roundedNumber(stats.getMovingVerticalSpeed()) + " " + vertSpeedUnitsStr); } - else { + else + { // no vertical speed available _totalVertSpeedLabel.setText(""); _movingVertSpeedLabel.setText(""); } } - - /** - * Format a number to a sensible precision - * @param inDist distance - * @return formatted String - */ - private String roundedNumber(double inDist) - { - // Set precision of formatter - int numDigits = 0; - if (inDist < 1.0) - numDigits = 3; - else if (inDist < 10.0) - numDigits = 2; - else if (inDist < 100.0) - numDigits = 1; - // set formatter - _distanceFormatter.setMaximumFractionDigits(numDigits); - _distanceFormatter.setMinimumFractionDigits(numDigits); - return _distanceFormatter.format(inDist); - } } diff --git a/tim/prune/function/GetWikipediaFunction.java b/tim/prune/function/GetWikipediaFunction.java index 0e6d9eb..4c11bf9 100644 --- a/tim/prune/function/GetWikipediaFunction.java +++ b/tim/prune/function/GetWikipediaFunction.java @@ -71,21 +71,48 @@ public class GetWikipediaFunction extends GenericDownloaderFunction lat = (coords[0] + coords[2]) / 2.0; lon = (coords[1] + coords[3]) / 2.0; } - else { + else + { lat = point.getLatitude().getDouble(); lon = point.getLongitude().getDouble(); } - String descMessage = ""; - InputStream inStream = null; + // Firstly try the local language + String lang = I18nManager.getText("wikipedia.lang"); + submitSearch(lat, lon, lang); + // If we didn't get anything, try a secondary language + if (_trackListModel.isEmpty() && _errorMessage == null && lang.equals("als")) { + submitSearch(lat, lon, "de"); + } + // If still nothing then try english + if (_trackListModel.isEmpty() && _errorMessage == null && !lang.equals("en")) { + submitSearch(lat, lon, "en"); + } + // Set status label according to error or "none found", leave blank if ok + if (_errorMessage == null && _trackListModel.isEmpty()) { + _errorMessage = I18nManager.getText("dialog.gpsies.nonefound"); + } + _statusLabel.setText(_errorMessage == null ? "" : _errorMessage); + } + + /** + * Submit the search for the given parameters + * @param inLat latitude + * @param inLon longitude + * @param inLang language code to use, such as en or de + */ + private void submitSearch(double inLat, double inLon, String inLang) + { // Example http://api.geonames.org/findNearbyWikipedia?lat=47&lng=9 String urlString = "http://api.geonames.org/findNearbyWikipedia?lat=" + - lat + "&lng=" + lon + "&maxRows=" + MAX_RESULTS - + "&radius=" + MAX_DISTANCE + "&lang=" + I18nManager.getText("wikipedia.lang") + inLat + "&lng=" + inLon + "&maxRows=" + MAX_RESULTS + + "&radius=" + MAX_DISTANCE + "&lang=" + inLang + "&username=" + GEONAMES_USERNAME; // Parse the returned XML with a special handler GetWikipediaXmlHandler xmlHandler = new GetWikipediaXmlHandler(); + InputStream inStream = null; + try { URL url = new URL(urlString); @@ -94,7 +121,7 @@ public class GetWikipediaFunction extends GenericDownloaderFunction saxParser.parse(inStream, xmlHandler); } catch (Exception e) { - descMessage = e.getClass().getName() + " - " + e.getMessage(); + _errorMessage = e.getClass().getName() + " - " + e.getMessage(); } // Close stream and ignore errors try { @@ -104,21 +131,18 @@ public class GetWikipediaFunction extends GenericDownloaderFunction ArrayList trackList = xmlHandler.getTrackList(); _trackListModel.addTracks(trackList); - // Set status label according to error or "none found", leave blank if ok - if (descMessage.equals("") && (trackList == null || trackList.size() == 0)) { - descMessage = I18nManager.getText("dialog.gpsies.nonefound"); - } - _statusLabel.setText(descMessage); // Show error message if any - if (trackList == null || trackList.size() == 0) { + if (_trackListModel.isEmpty()) + { String error = xmlHandler.getErrorMessage(); - if (error != null && !error.equals("")) { + if (error != null && !error.equals("")) + { _app.showErrorMessageNoLookup(getNameKey(), error); + _errorMessage = error; } } } - /** * Load the selected point(s) */ diff --git a/tim/prune/function/PasteCoordinates.java b/tim/prune/function/PasteCoordinates.java index 7587949..0228014 100644 --- a/tim/prune/function/PasteCoordinates.java +++ b/tim/prune/function/PasteCoordinates.java @@ -29,6 +29,8 @@ import tim.prune.data.DataPoint; import tim.prune.data.Field; import tim.prune.data.Latitude; import tim.prune.data.Longitude; +import tim.prune.data.Unit; +import tim.prune.data.UnitSetLibrary; import tim.prune.gui.GuiGridLayout; /** @@ -75,7 +77,7 @@ public class PasteCoordinates extends GenericFunction // MAYBE: Paste clipboard into the edit field _coordField.setText(""); _nameField.setText(""); - boolean useMetres = (Config.getUnitSet().getDefaultAltitudeFormat() == Altitude.Format.METRES); + boolean useMetres = (Config.getUnitSet().getAltitudeUnit() == UnitSetLibrary.UNITS_METRES); _altUnitsDropDown.setSelectedIndex(useMetres?0:1); enableOK(); _dialog.setVisible(true); @@ -236,9 +238,9 @@ public class PasteCoordinates extends GenericFunction if (inValue3 != null) { // Look at altitude units dropdown - final Altitude.Format altFormat = (_altUnitsDropDown.getSelectedIndex()==0? - Altitude.Format.METRES:Altitude.Format.FEET); - alt = new Altitude(inValue3, altFormat); + final Unit altUnit = (_altUnitsDropDown.getSelectedIndex()==0? + UnitSetLibrary.UNITS_METRES : UnitSetLibrary.UNITS_FEET); + alt = new Altitude(inValue3, altUnit); if (!alt.isValid()) {alt = null;} } // See if value1 can be lat and value2 lon: diff --git a/tim/prune/function/SearchWikipediaNames.java b/tim/prune/function/SearchWikipediaNames.java index 0458943..5712983 100644 --- a/tim/prune/function/SearchWikipediaNames.java +++ b/tim/prune/function/SearchWikipediaNames.java @@ -67,7 +67,7 @@ public class SearchWikipediaNames extends GenericDownloaderFunction JOptionPane.QUESTION_MESSAGE, null, null, ""); if (search != null) { - _searchTerm = search.toString(); + _searchTerm = search.toString().toLowerCase(); if (!_searchTerm.equals("")) { super.begin(); } @@ -81,29 +81,43 @@ public class SearchWikipediaNames extends GenericDownloaderFunction { _statusLabel.setText(I18nManager.getText("confirm.running")); - String descMessage = ""; - InputStream inStream = null; + // Replace awkward characters with character equivalents + final String searchTerm = encodeSearchTerm(_searchTerm); - // language (only de and en available) + // Firstly try the local language String lang = I18nManager.getText("wikipedia.lang"); - if (lang.equals("de") || lang.equals("als")) { - lang = "de"; + submitSearch(searchTerm, lang); + // If we didn't get anything, try a secondary language + if (_trackListModel.isEmpty() && _errorMessage == null && lang.equals("als")) { + submitSearch(searchTerm, "de"); } - else { - lang = "en"; + // If still nothing then try english + if (_trackListModel.isEmpty() && _errorMessage == null && !lang.equals("en")) { + submitSearch(searchTerm, "en"); } - // Replace awkward characters with character equivalents - String searchTerm; - try { - searchTerm = URLEncoder.encode(_searchTerm, "UTF-8"); - } catch (UnsupportedEncodingException e1) { - searchTerm = _searchTerm; + + // Set status label according to error or "none found", leave blank if ok + if (_errorMessage == null && _trackListModel.isEmpty()) { + _errorMessage = I18nManager.getText("dialog.gpsies.nonefound"); } - // Example http://ws.geonames.org/wikipediaSearch?q=london&maxRows=10 - String urlString = "http://api.geonames.org/wikipediaSearch?title=" + searchTerm - + "&maxRows=" + MAX_RESULTS + "&lang=" + lang + "&username=" + GEONAMES_USERNAME; + _statusLabel.setText(_errorMessage == null ? "" : _errorMessage); + } + + /** + * Submit the given search to the server + * @param inSearchTerm search term + * @param inLang language code such as en, de + */ + private void submitSearch(String inSearchTerm, String inLang) + { + // System.out.println("Searching for '" + inSearchTerm + "' with lang: " + inLang); + + String urlString = "http://api.geonames.org/wikipediaSearch?title=" + inSearchTerm + + "&maxRows=" + MAX_RESULTS + "&lang=" + inLang + "&username=" + GEONAMES_USERNAME; // Parse the returned XML with a special handler GetWikipediaXmlHandler xmlHandler = new GetWikipediaXmlHandler(); + InputStream inStream = null; + try { URL url = new URL(urlString); @@ -112,7 +126,7 @@ public class SearchWikipediaNames extends GenericDownloaderFunction saxParser.parse(inStream, xmlHandler); } catch (Exception e) { - descMessage = e.getClass().getName() + " - " + e.getMessage(); + _errorMessage = e.getClass().getName() + " - " + e.getMessage(); } // Close stream and ignore errors try { @@ -122,15 +136,59 @@ public class SearchWikipediaNames extends GenericDownloaderFunction ArrayList trackList = xmlHandler.getTrackList(); // TODO: Do a better job of sorting replies by relevance - use three different lists _trackListModel.addTracks(trackList); + } - // Set status label according to error or "none found", leave blank if ok - if (descMessage.equals("") && (trackList == null || trackList.size() == 0)) { - descMessage = I18nManager.getText("dialog.gpsies.nonefound"); + /** + * Replace tricky characters in the search term and encode the others + * @param inSearchTerm entered search term + * @return modified search term to give to geonames + */ + private static final String encodeSearchTerm(String inSearchTerm) + { + if (inSearchTerm != null && inSearchTerm.length() > 0) + { + // Replace umlauts oe, ue, ae, OE, UE, AE, szlig + StringBuilder sb = new StringBuilder(); + final int numChars = inSearchTerm.length(); + for (int i=0; i0 && _heightField.getValue()>0); - } - }; - MouseAdapter mouseListener = new MouseAdapter() { - public void mouseReleased(java.awt.event.MouseEvent arg0) { - _okButton.setEnabled(_widthField.getValue()>0 && _heightField.getValue()>0); - }; - }; - _widthField.addKeyListener(keyListener); - _heightField.addKeyListener(keyListener); - _widthField.addMouseListener(mouseListener); - _heightField.addMouseListener(mouseListener); - - // button panel at bottom - JPanel buttonPanel = new JPanel(); - buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); - _okButton = new JButton(I18nManager.getText("button.ok")); - ActionListener okListener = new ActionListener() { - public void actionPerformed(ActionEvent e) - { - finish(); - } - }; - _okButton.addActionListener(okListener); - _okButton.setEnabled(false); - buttonPanel.add(_okButton); - JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); - cancelButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) - { - _dialog.dispose(); - } - }); - buttonPanel.add(cancelButton); - dialogPanel.add(buttonPanel, BorderLayout.SOUTH); - dialogPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15)); - return dialogPanel; - } - - /** - * @param inKey text key - * @return right-aligned label - */ - private static final JLabel makeRightLabel(String inKey) - { - JLabel label = new JLabel(I18nManager.getText(inKey) + " : "); - label.setHorizontalAlignment(SwingConstants.RIGHT); - return label; - } - - - /** - * Finish the dialog when OK pressed - */ - private void finish() - { - if (_widthField.getValue() > 0 && _heightField.getValue() > 0) - { - // Set entered values in Config - Config.setConfigInt(Config.KEY_KMZ_IMAGE_WIDTH, _widthField.getValue()); - Config.setConfigInt(Config.KEY_KMZ_IMAGE_HEIGHT, _heightField.getValue()); - _dialog.dispose(); - } - } -} diff --git a/tim/prune/function/SetLanguage.java b/tim/prune/function/SetLanguage.java index 4407808..3962787 100644 --- a/tim/prune/function/SetLanguage.java +++ b/tim/prune/function/SetLanguage.java @@ -45,11 +45,11 @@ public class SetLanguage extends GenericFunction "espa\u00F1ol", "fran\u00E7ais", "italiano", "magyar", "nederlands", "polski", "portugu\u00EAs", "\u0440\u0443\u0441\u0441\u043a\u0438\u0439 (russian)", "\u4e2d\u6587 (chinese)", "\u65E5\u672C\u8A9E (japanese)", "\uD55C\uAD6D\uC5B4/\uC870\uC120\uB9D0 (korean)", "schwiizerd\u00FC\u00FCtsch", "t\u00FCrk\u00E7e", - "rom\u00E2n\u0103", "afrikaans", "bahasa indonesia" + "afrikaans", "rom\u00E2n\u0103" }; /** Associated language codes (must be in same order as names!) */ private static final String[] LANGUAGE_CODES = {"cz", "de", "en", "en_us", "es", "fr", "it", "hu", - "nl", "pl", "pt", "ru", "zh", "ja", "ko", "de_ch", "tr", "ro", "af", "in" + "nl", "pl", "pt", "ru", "zh", "ja", "ko", "de_ch", "tr", "af", "ro" }; diff --git a/tim/prune/function/edit/EditFieldsTableModel.java b/tim/prune/function/edit/EditFieldsTableModel.java index f4ee561..4fb46d7 100644 --- a/tim/prune/function/edit/EditFieldsTableModel.java +++ b/tim/prune/function/edit/EditFieldsTableModel.java @@ -10,6 +10,7 @@ import tim.prune.I18nManager; public class EditFieldsTableModel extends AbstractTableModel { private String[] _fieldNames = null; + private String[] _originalValues = null; private String[] _fieldValues = null; private boolean[] _valueChanged = null; @@ -20,9 +21,10 @@ public class EditFieldsTableModel extends AbstractTableModel */ public EditFieldsTableModel(int inSize) { - _fieldNames = new String[inSize]; - _fieldValues = new String[inSize]; - _valueChanged = new boolean[inSize]; + _fieldNames = new String[inSize]; + _originalValues = new String[inSize]; + _fieldValues = new String[inSize]; + _valueChanged = new boolean[inSize]; } @@ -35,6 +37,7 @@ public class EditFieldsTableModel extends AbstractTableModel public void addFieldInfo(String inName, String inValue, int inIndex) { _fieldNames[inIndex] = inName; + _originalValues[inIndex] = inValue; _fieldValues[inIndex] = inValue; _valueChanged[inIndex] = false; } @@ -45,7 +48,7 @@ public class EditFieldsTableModel extends AbstractTableModel */ public int getColumnCount() { - return 3; + return 2; } @@ -67,11 +70,7 @@ public class EditFieldsTableModel extends AbstractTableModel { return _fieldNames[inRowIndex]; } - else if (inColumnIndex == 1) - { - return _fieldValues[inRowIndex]; - } - return Boolean.valueOf(_valueChanged[inRowIndex]); + return _fieldValues[inRowIndex]; } @@ -111,8 +110,7 @@ public class EditFieldsTableModel extends AbstractTableModel public String getColumnName(int inColNum) { if (inColNum == 0) return I18nManager.getText("dialog.pointedit.table.field"); - else if (inColNum == 1) return I18nManager.getText("dialog.pointedit.table.value"); - return I18nManager.getText("dialog.pointedit.table.changed"); + return I18nManager.getText("dialog.pointedit.table.value"); } @@ -120,28 +118,36 @@ public class EditFieldsTableModel extends AbstractTableModel * Update the value of the given row * @param inRowNum number of row, starting at 0 * @param inValue new value - * @return true if data updated */ - public boolean updateValue(int inRowNum, String inValue) + public void updateValue(int inRowNum, String inValue) { + String origValue = _originalValues[inRowNum]; String currValue = _fieldValues[inRowNum]; - // ignore empty-to-empty changes - if ((currValue == null || currValue.equals("")) && (inValue == null || inValue.equals(""))) + // Update model if changed from original value + _valueChanged[inRowNum] = areStringsDifferent(origValue, inValue); + // Update model if changed from current value + if (areStringsDifferent(currValue, inValue)) { - return false; - } - // ignore changes when strings equal - if (currValue == null || inValue == null || !currValue.equals(inValue)) - { - // really changed _fieldValues[inRowNum] = inValue; - _valueChanged[inRowNum] = true; fireTableRowsUpdated(inRowNum, inRowNum); - return true; } - return false; } + /** + * Compare two strings to see if they're equal or not (nulls treated the same as empty strings) + * @param inString1 first string + * @param inString2 second string + * @return true if the strings are different + */ + private static boolean areStringsDifferent(String inString1, String inString2) + { + // if both empty then same + if ((inString1 == null || inString1.equals("")) && (inString2 == null || inString2.equals(""))) + { + return false; + } + return (inString1 == null || inString2 == null || !inString1.equals(inString2)); + } /** * Get the value at the given index diff --git a/tim/prune/function/edit/PointEditor.java b/tim/prune/function/edit/PointEditor.java index 2321ece..40cf12b 100644 --- a/tim/prune/function/edit/PointEditor.java +++ b/tim/prune/function/edit/PointEditor.java @@ -1,31 +1,39 @@ package tim.prune.function.edit; import java.awt.BorderLayout; +import java.awt.Color; 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.BorderFactory; +import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLabel; -import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; +import javax.swing.JTextArea; +import javax.swing.JTextField; import javax.swing.ListSelectionModel; +import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; +import javax.swing.table.TableCellRenderer; import tim.prune.App; import tim.prune.I18nManager; +import tim.prune.config.Config; import tim.prune.data.DataPoint; import tim.prune.data.Field; import tim.prune.data.FieldList; import tim.prune.data.Track; +import tim.prune.data.Unit; /** * Class to manage the display and editing of point data @@ -36,11 +44,14 @@ public class PointEditor private JFrame _parentFrame = null; private JDialog _dialog = null; private JTable _table = null; + private JLabel _fieldnameLabel = null; + private JTextField _valueField = null; + private JTextArea _valueArea = null; + private JScrollPane _valueAreaPane = null; private Track _track = null; private DataPoint _point = null; private EditFieldsTableModel _model = null; - private JButton _editButton = null; - private JButton _okButton = null; + private int _prevRowIndex = -1; /** @@ -76,9 +87,16 @@ public class PointEditor Field field = fieldList.getField(i); _model.addFieldInfo(field.getName(), _point.getFieldValue(field), i); } - // Create Gui and show it + // Create Gui _dialog.getContentPane().add(makeDialogComponents()); _dialog.pack(); + // Init right-hand side + SwingUtilities.invokeLater(new Runnable() { + public void run() { + _valueField.setVisible(false); + _valueAreaPane.setVisible(false); + } + }); _dialog.setVisible(true); } @@ -90,44 +108,71 @@ public class PointEditor private Component makeDialogComponents() { JPanel panel = new JPanel(); - panel.setLayout(new BorderLayout(1, 10)); + panel.setLayout(new BorderLayout(20, 10)); // Create GUI layout for point editor - _table = new JTable(_model); + _table = new JTable(_model) + { + // Paint the changed fields orange + public Component prepareRenderer(TableCellRenderer renderer, int row, int column) + { + Component comp = super.prepareRenderer(renderer, row, column); + boolean changed = ((EditFieldsTableModel) getModel()).getChanged(row); + comp.setBackground(changed ? Color.orange : getBackground()); + return comp; + } + }; _table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + _table.getSelectionModel().clearSelection(); _table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { - // enable edit button when row selected - _editButton.setEnabled(true); + fieldSelected(); } }); - _table.setPreferredScrollableViewportSize(new Dimension(_table.getWidth(), _table.getRowHeight() * 6)); - panel.add(new JScrollPane(_table), BorderLayout.CENTER); + _table.setPreferredScrollableViewportSize(new Dimension(_table.getWidth() * 2, _table.getRowHeight() * 6)); + JScrollPane tablePane = new JScrollPane(_table); + tablePane.setPreferredSize(new Dimension(150, 100)); + // Label at top - JLabel topLabel = new JLabel(I18nManager.getText("dialog.pointedit.text")); + JLabel topLabel = new JLabel(I18nManager.getText("dialog.pointedit.intro")); topLabel.setBorder(BorderFactory.createEmptyBorder(8, 6, 3, 6)); panel.add(topLabel, BorderLayout.NORTH); - _editButton = new JButton(I18nManager.getText("button.edit")); - _editButton.addActionListener(new ActionListener() { + + // listener for ok event + ActionListener okListener = new ActionListener() { public void actionPerformed(ActionEvent e) { - // Update field value and enable ok button - String currValue = _model.getValue(_table.getSelectedRow()); - Object newValue = JOptionPane.showInputDialog(_dialog, - I18nManager.getText("dialog.pointedit.changevalue.text"), - I18nManager.getText("dialog.pointedit.changevalue.title"), - JOptionPane.QUESTION_MESSAGE, null, null, currValue); - if (newValue != null - && _model.updateValue(_table.getSelectedRow(), newValue.toString())) - { - _okButton.setEnabled(true); - } + // update App with edit + confirmEdit(); + _dialog.dispose(); } - }); - _editButton.setEnabled(false); + }; + JPanel rightPanel = new JPanel(); - rightPanel.add(_editButton); - panel.add(rightPanel, BorderLayout.EAST); + rightPanel.setLayout(new BorderLayout()); + JPanel rightiPanel = new JPanel(); + rightiPanel.setLayout(new BoxLayout(rightiPanel, BoxLayout.Y_AXIS)); + // Add GUI elements to rhs + _fieldnameLabel = new JLabel(I18nManager.getText("dialog.pointedit.nofield")); + rightiPanel.add(_fieldnameLabel); + _valueField = new JTextField(11); + // Add listener for enter button + _valueField.addActionListener(okListener); + rightiPanel.add(_valueField); + rightPanel.add(rightiPanel, BorderLayout.NORTH); + _valueArea = new JTextArea(5, 15); + _valueArea.setLineWrap(true); + _valueArea.setWrapStyleWord(true); + _valueAreaPane = new JScrollPane(_valueArea); + rightPanel.add(_valueAreaPane, BorderLayout.CENTER); + + // Put the table and the right-hand panel together in a grid + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new GridLayout(0, 2, 10, 10)); + mainPanel.add(tablePane); + mainPanel.add(rightPanel); + panel.add(mainPanel, BorderLayout.CENTER); + // Bottom panel for OK, cancel buttons JPanel lowerPanel = new JPanel(); lowerPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); @@ -139,27 +184,109 @@ public class PointEditor } }); 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(); - } - }); - lowerPanel.add(_okButton); + JButton okButton = new JButton(I18nManager.getText("button.ok")); + okButton.addActionListener(okListener); + lowerPanel.add(okButton); panel.add(lowerPanel, BorderLayout.SOUTH); return panel; } + /** + * When table selection changes, need to update model and go to the selected field + */ + private void fieldSelected() + { + int rowNum = _table.getSelectedRow(); + if (rowNum == _prevRowIndex) {return;} // selection hasn't changed + // Check the current values + if (_prevRowIndex >= 0) + { + Field prevField = _track.getFieldList().getField(_prevRowIndex); + boolean littleField = prevField.isBuiltIn() && prevField != Field.DESCRIPTION; + String newValue = littleField ? _valueField.getText() : _valueArea.getText(); + // Update the model from the current GUI values + _model.updateValue(_prevRowIndex, newValue); + } + + if (rowNum < 0) + { + _fieldnameLabel.setText(""); + } + else + { + String currValue = _model.getValue(rowNum); + Field field = _track.getFieldList().getField(rowNum); + _fieldnameLabel.setText(makeFieldLabel(field, _point)); + _fieldnameLabel.setVisible(true); + boolean littleField = field.isBuiltIn() && field != Field.DESCRIPTION; + if (littleField) { + _valueField.setText(currValue); + } + else { + _valueArea.setText(currValue); + } + _valueField.setVisible(littleField); + _valueAreaPane.setVisible(!littleField); + if (littleField) { + _valueField.requestFocus(); + } + else { + _valueArea.requestFocus(); + } + } + _prevRowIndex = rowNum; + } + + /** + * @param inField field + * @param inPoint current point + * @return label string for above the entry field / area + */ + private static String makeFieldLabel(Field inField, DataPoint inPoint) + { + String label = I18nManager.getText("dialog.pointedit.table.field") + ": " + inField.getName(); + // Add units if the field is altitude / speed / vspeed + if (inField == Field.ALTITUDE) + { + label += makeUnitsLabel(inPoint.hasAltitude() ? inPoint.getAltitude().getUnit() : Config.getUnitSet().getAltitudeUnit()); + } + else if (inField == Field.SPEED) + { + label += makeUnitsLabel(inPoint.hasHSpeed() ? inPoint.getHSpeed().getUnit() : Config.getUnitSet().getSpeedUnit()); + } + else if (inField == Field.VERTICAL_SPEED) + { + label += makeUnitsLabel(inPoint.hasVSpeed() ? inPoint.getVSpeed().getUnit() : Config.getUnitSet().getVerticalSpeedUnit()); + } + return label; + } + + /** + * @param inUnit units for altitude / speed + * @return addition to the field label to describe the units + */ + private static String makeUnitsLabel(Unit inUnit) + { + if (inUnit == null) return ""; + return " (" + I18nManager.getText(inUnit.getShortnameKey()) + ")"; + } + /** * Confirm the edit and inform the app */ private void confirmEdit() { + // Apply the edits to the current field + int rowNum = _table.getSelectedRow(); + if (rowNum >= 0) + { + Field currField = _track.getFieldList().getField(rowNum); + boolean littleField = currField.isBuiltIn() && currField != Field.DESCRIPTION; + String newValue = littleField ? _valueField.getText() : _valueArea.getText(); + _model.updateValue(_prevRowIndex, newValue); + } + // Package the modified fields into an object FieldList fieldList = _track.getFieldList(); int numFields = fieldList.getNumFields(); diff --git a/tim/prune/function/edit/PointNameEditor.java b/tim/prune/function/edit/PointNameEditor.java index bfa46c4..53e1f83 100644 --- a/tim/prune/function/edit/PointNameEditor.java +++ b/tim/prune/function/edit/PointNameEditor.java @@ -8,6 +8,7 @@ import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; +import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JDialog; @@ -77,7 +78,8 @@ public class PointNameEditor extends GenericFunction panel.setLayout(new BorderLayout()); // Create GUI layout for point name editor JPanel centrePanel = new JPanel(); - centrePanel.add(new JLabel(I18nManager.getText("dialog.pointnameedit.name") + ":")); + centrePanel.setLayout(new BorderLayout(8, 8)); + centrePanel.add(new JLabel(I18nManager.getText("dialog.pointnameedit.name") + ": "), BorderLayout.WEST); // Make listener to react to ok being pressed ActionListener okActionListener = new ActionListener() { public void actionPerformed(ActionEvent e) @@ -101,8 +103,13 @@ public class PointNameEditor extends GenericFunction } }); _nameField.addActionListener(okActionListener); - centrePanel.add(_nameField); - panel.add(centrePanel); + centrePanel.add(_nameField, BorderLayout.CENTER); + // holder panel to stop the text box from being stretched + JPanel holderPanel = new JPanel(); + holderPanel.setLayout(new BorderLayout()); + holderPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + holderPanel.add(centrePanel, BorderLayout.NORTH); + panel.add(holderPanel, BorderLayout.CENTER); JPanel rightPanel = new JPanel(); rightPanel.setLayout(new BoxLayout(rightPanel, BoxLayout.Y_AXIS)); JButton upperButton = new JButton(I18nManager.getText("dialog.pointnameedit.uppercase")); diff --git a/tim/prune/function/estimate/EstimateTime.java b/tim/prune/function/estimate/EstimateTime.java new file mode 100644 index 0000000..7a502e4 --- /dev/null +++ b/tim/prune/function/estimate/EstimateTime.java @@ -0,0 +1,363 @@ +package tim.prune.function.estimate; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +import tim.prune.App; +import tim.prune.GenericFunction; +import tim.prune.I18nManager; +import tim.prune.config.Config; +import tim.prune.data.RangeStats; +import tim.prune.data.Selection; +import tim.prune.data.Unit; +import tim.prune.gui.DecimalNumberField; +import tim.prune.gui.DisplayUtils; +import tim.prune.gui.GuiGridLayout; + +/** + * Class to calculate and show the results of estimating (hike) time for the current range + */ +public class EstimateTime extends GenericFunction +{ + /** Dialog */ + private JDialog _dialog = null; + /** Labels for distances */ + private JLabel _distanceLabel = null; + /** Labels for durations */ + private JLabel _estimatedDurationLabel = null, _actualDurationLabel = null; + /** Labels for climbs */ + private JLabel _gentleClimbLabel = null, _steepClimbLabel = null; + /** Labels for descents */ + private JLabel _gentleDescentLabel = null, _steepDescentLabel = null; + /** Labels and text fields for parameters */ + private JLabel _flatSpeedLabel = null; + private DecimalNumberField _flatSpeedField = null; + private JLabel _climbParamLabel = null; + private DecimalNumberField _gentleClimbField = null, _steepClimbField = null; + private JLabel _descentParamLabel = null; + private DecimalNumberField _gentleDescentField = null, _steepDescentField = null; + /** Range stats */ + private RangeStats _stats = null; + + + /** + * Constructor + * @param inApp App object + */ + public EstimateTime(App inApp) + { + super(inApp); + } + + /** Get the name key */ + public String getNameKey() { + return "function.estimatetime"; + } + + /** + * Begin the function + */ + public void begin() + { + // Get the stats on the selection before launching the dialog + Selection selection = _app.getTrackInfo().getSelection(); + _stats = new RangeStats(_app.getTrackInfo().getTrack(), selection.getStart(), selection.getEnd()); + + if (_stats.getMovingDistance() < 0.01) + { + _app.showErrorMessage(getNameKey(), "dialog.estimatetime.error.nodistance"); + return; + } + if (_dialog == null) + { + // TODO: Check whether params are at default, show tip message if unaltered? + _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true); + _dialog.setLocationRelativeTo(_parentFrame); + _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + _dialog.getContentPane().add(makeDialogComponents()); + _dialog.pack(); + } + updateDetails(); + _dialog.setVisible(true); + } + + /** + * Create dialog components + * @return Panel containing all gui elements in dialog + */ + private Component makeDialogComponents() + { + JPanel dialogPanel = new JPanel(); + dialogPanel.setLayout(new BorderLayout(5, 5)); + + // main panel with a box layout + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); + // Label at top + JLabel introLabel = new JLabel(I18nManager.getText("dialog.fullrangedetails.intro") + ":"); + introLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + introLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + mainPanel.add(introLabel); + mainPanel.add(Box.createVerticalStrut(4)); + + // Details panel in a grid + JPanel detailsPanel = new JPanel(); + detailsPanel.setLayout(new GridLayout(0, 4, 6, 2)); + detailsPanel.setBorder(BorderFactory.createTitledBorder( + I18nManager.getText("dialog.estimatetime.details"))); + + // Distance + JLabel distLabel = new JLabel(I18nManager.getText("fieldname.distance") + ": "); + distLabel.setHorizontalAlignment(JLabel.RIGHT); + detailsPanel.add(distLabel); + _distanceLabel = new JLabel("5 km"); + detailsPanel.add(_distanceLabel); + detailsPanel.add(new JLabel("")); detailsPanel.add(new JLabel("")); // two blank cells + + detailsPanel.add(new JLabel("")); + detailsPanel.add(new JLabel(I18nManager.getText("dialog.estimatetime.gentle"))); + detailsPanel.add(new JLabel(I18nManager.getText("dialog.estimatetime.steep"))); + detailsPanel.add(new JLabel("")); // blank cells + + // Climb + JLabel climbLabel = new JLabel(I18nManager.getText("dialog.estimatetime.climb") + ": "); + climbLabel.setHorizontalAlignment(JLabel.RIGHT); + detailsPanel.add(climbLabel); + _gentleClimbLabel = new JLabel("1500 m"); + detailsPanel.add(_gentleClimbLabel); + _steepClimbLabel = new JLabel("1500 m"); + detailsPanel.add(_steepClimbLabel); + detailsPanel.add(new JLabel("")); + + // Descent + JLabel descentLabel = new JLabel(I18nManager.getText("dialog.estimatetime.descent") + ": "); + descentLabel.setHorizontalAlignment(JLabel.RIGHT); + detailsPanel.add(descentLabel); + _gentleDescentLabel = new JLabel("1500 m"); + detailsPanel.add(_gentleDescentLabel); + _steepDescentLabel = new JLabel("1500 m"); + detailsPanel.add(_steepDescentLabel); + detailsPanel.add(new JLabel("")); + + detailsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + mainPanel.add(detailsPanel); + mainPanel.add(Box.createVerticalStrut(4)); + + // Parameters panel in a flexible grid + JPanel paramsPanel = new JPanel(); + GuiGridLayout paramsGrid = new GuiGridLayout(paramsPanel, new double[] {1.5, 0.2, 1.0, 0.2, 0.5}, + new boolean[] {true, false, false, false, false}); + paramsPanel.setBorder(BorderFactory.createTitledBorder( + I18nManager.getText("dialog.estimatetime.parameters"))); + KeyAdapter paramChangeListener = new KeyAdapter() { + public void keyTyped(KeyEvent inE) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + calculateEstimatedTime(); + } + }); + } + public void keyPressed(KeyEvent inE) { + if (inE.getKeyCode() == KeyEvent.VK_ESCAPE) {_dialog.dispose();} + } + }; + + // Flat speed + _flatSpeedLabel = new JLabel(I18nManager.getText("dialog.estimatetime.parameters.timefor") + ": "); // (filled in later) + _flatSpeedLabel.setHorizontalAlignment(JLabel.RIGHT); + paramsGrid.add(_flatSpeedLabel); + _flatSpeedField = new DecimalNumberField(); + _flatSpeedField.addKeyListener(paramChangeListener); + paramsGrid.add(_flatSpeedField); + JLabel minsLabel = new JLabel(I18nManager.getText("units.minutes")); + paramsGrid.add(minsLabel); + paramsGrid.nextRow(); + // Headers for gentle and steep + paramsGrid.add(new JLabel("")); + paramsGrid.add(new JLabel(I18nManager.getText("dialog.estimatetime.gentle"))); + paramsGrid.add(new JLabel("")); // blank cell + paramsGrid.add(new JLabel(I18nManager.getText("dialog.estimatetime.steep"))); + paramsGrid.nextRow(); + // Gentle climb + _climbParamLabel = new JLabel(I18nManager.getText("dialog.estimatetime.parameters.timefor") + ": "); // (filled in later) + _climbParamLabel.setHorizontalAlignment(JLabel.RIGHT); + paramsGrid.add(_climbParamLabel); + _gentleClimbField = new DecimalNumberField(); + _gentleClimbField.addKeyListener(paramChangeListener); + paramsGrid.add(_gentleClimbField); + paramsGrid.add(new JLabel(minsLabel.getText())); + // Steep climb + _steepClimbField = new DecimalNumberField(); + _steepClimbField.addKeyListener(paramChangeListener); + paramsGrid.add(_steepClimbField); + paramsGrid.add(new JLabel(minsLabel.getText())); + + // Gentle descent + _descentParamLabel = new JLabel(I18nManager.getText("dialog.estimatetime.parameters.timefor") + ": "); // (filled in later) + _descentParamLabel.setHorizontalAlignment(JLabel.RIGHT); + paramsGrid.add(_descentParamLabel); + _gentleDescentField = new DecimalNumberField(true); // negative numbers allowed + _gentleDescentField.addKeyListener(paramChangeListener); + paramsGrid.add(_gentleDescentField); + paramsGrid.add(new JLabel(minsLabel.getText())); + // Steep climb + _steepDescentField = new DecimalNumberField(true); // negative numbers allowed + _steepDescentField.addKeyListener(paramChangeListener); + paramsGrid.add(_steepDescentField); + paramsGrid.add(new JLabel(minsLabel.getText())); + + paramsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + mainPanel.add(paramsPanel); + mainPanel.add(Box.createVerticalStrut(12)); + + // Results panel + JPanel resultsPanel = new JPanel(); + resultsPanel.setBorder(BorderFactory.createTitledBorder( + I18nManager.getText("dialog.estimatetime.results"))); + resultsPanel.setLayout(new GridLayout(0, 2, 3, 3)); + // estimated time + _estimatedDurationLabel = new JLabel(I18nManager.getText("dialog.estimatetime.results.estimatedtime") + ": "); // filled in later + Font origFont = _estimatedDurationLabel.getFont(); + _estimatedDurationLabel.setFont(origFont.deriveFont(Font.BOLD, origFont.getSize2D() + 2.0f)); + + resultsPanel.add(_estimatedDurationLabel); + // actual time (if available) + _actualDurationLabel = new JLabel(I18nManager.getText("dialog.estimatetime.results.actualtime") + ": "); // filled in later + resultsPanel.add(_actualDurationLabel); + + resultsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + mainPanel.add(resultsPanel); + mainPanel.add(Box.createVerticalStrut(4)); + + dialogPanel.add(mainPanel, BorderLayout.NORTH); + // button panel at bottom + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + JButton closeButton = new JButton(I18nManager.getText("button.close")); + closeButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + finishDialog(); + } + }); + closeButton.addKeyListener(new KeyAdapter() { + public void keyPressed(KeyEvent inE) { + if (inE.getKeyCode() == KeyEvent.VK_ESCAPE) {_dialog.dispose();} + } + }); + buttonPanel.add(closeButton); + dialogPanel.add(buttonPanel, BorderLayout.SOUTH); + return dialogPanel; + } + + + /** + * Recalculate the values and update the labels + */ + private void updateDetails() + { + // Warn if the current track hasn't got any height information + if (!_stats.getMovingAltitudeRange().hasRange()) { + _app.showErrorMessage(getNameKey(), "dialog.estimatetime.error.noaltitudes"); + } + + // Distance in current units + final Unit distUnit = Config.getUnitSet().getDistanceUnit(); + final String distUnitsStr = I18nManager.getText(distUnit.getShortnameKey()); + final double movingDist = _stats.getMovingDistance(); + _distanceLabel.setText(DisplayUtils.roundedNumber(movingDist) + " " + distUnitsStr); + + // Climb and descent values + final Unit altUnit = Config.getUnitSet().getAltitudeUnit(); + final String altUnitsStr = " " + I18nManager.getText(altUnit.getShortnameKey()); + _gentleClimbLabel.setText(_stats.getGentleAltitudeRange().getClimb(altUnit) + altUnitsStr); + _steepClimbLabel.setText(_stats.getSteepAltitudeRange().getClimb(altUnit) + altUnitsStr); + _gentleDescentLabel.setText(_stats.getGentleAltitudeRange().getDescent(altUnit) + altUnitsStr); + _steepDescentLabel.setText(_stats.getSteepAltitudeRange().getDescent(altUnit) + altUnitsStr); + + // Try to get parameters from config + EstimationParameters estParams = new EstimationParameters(Config.getConfigString(Config.KEY_ESTIMATION_PARAMS)); + + String[] paramValues = estParams.getStrings(); + if (paramValues == null || paramValues.length != 5) {return;} // TODO: What to do in case of error? + // Flat time is either for 5 km, 3 miles or 3 nm + _flatSpeedLabel.setText(I18nManager.getText("dialog.estimatetime.parameters.timefor") + + " " + EstimationParameters.getStandardDistance() + ": "); + _flatSpeedField.setText(paramValues[0]); + + final String heightString = " " + EstimationParameters.getStandardClimb() + ": "; + _climbParamLabel.setText(I18nManager.getText("dialog.estimatetime.climb") + heightString); + _gentleClimbField.setText(paramValues[1]); + _steepClimbField.setText(paramValues[2]); + _descentParamLabel.setText(I18nManager.getText("dialog.estimatetime.descent") + heightString); + _gentleDescentField.setText(paramValues[3]); + _steepDescentField.setText(paramValues[4]); + + // Use the entered parameters to estimate the time + calculateEstimatedTime(); + + // Get the actual time if available, for comparison + if (_stats.getMovingDurationInSeconds() > 0) + { + _actualDurationLabel.setText(I18nManager.getText("dialog.estimatetime.results.actualtime") + ": " + + DisplayUtils.buildDurationString(_stats.getMovingDurationInSeconds())); + } + else { + _actualDurationLabel.setText(""); + } + } + + + /** + * Use the current parameter and the range stats to calculate the estimated time + * and populate the answer in the dialog + */ + private void calculateEstimatedTime() + { + // Populate an EstimationParameters object from the four strings + EstimationParameters params = new EstimationParameters(); + params.populateWithStrings(_flatSpeedField.getText(), _gentleClimbField.getText(), + _steepClimbField.getText(), _gentleDescentField.getText(), _steepDescentField.getText()); + if (!params.isValid()) { + _estimatedDurationLabel.setText("- - -"); + } + else + { + final long numSeconds = (long) (params.applyToStats(_stats) * 60.0); + _estimatedDurationLabel.setText(I18nManager.getText("dialog.estimatetime.results.estimatedtime") + ": " + + DisplayUtils.buildDurationString(numSeconds)); + } + } + + + /** + * Finish with the dialog, by pressing the "Close" button + */ + private void finishDialog() + { + // Make estimation parameters from entered strings, if valid save to config + EstimationParameters params = new EstimationParameters(); + params.populateWithStrings(_flatSpeedField.getText(), _gentleClimbField.getText(), + _steepClimbField.getText(), _gentleDescentField.getText(), _steepDescentField.getText()); + if (params.isValid()) { + Config.setConfigString(Config.KEY_ESTIMATION_PARAMS, params.toConfigString()); + } + _dialog.dispose(); + } +} diff --git a/tim/prune/function/estimate/EstimationParameters.java b/tim/prune/function/estimate/EstimationParameters.java new file mode 100644 index 0000000..e78994b --- /dev/null +++ b/tim/prune/function/estimate/EstimationParameters.java @@ -0,0 +1,278 @@ +package tim.prune.function.estimate; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Locale; + +import tim.prune.I18nManager; +import tim.prune.config.Config; +import tim.prune.data.RangeStats; +import tim.prune.data.Unit; +import tim.prune.data.UnitSetLibrary; + +/** + * Class to hold, parse and convert the parameters for time estimation. + * These are five (metric) values which can be loaded and saved from config, + * and are used by the EstimateTime function + */ +public class EstimationParameters +{ + /** Minutes required for flat travel, fixed metric distance */ + private double _flatMins = 0.0; + /** Minutes required for climbing, fixed metric distance */ + private double _gentleClimbMins = 0.0, _steepClimbMins; + /** Minutes required for descending, fixed metric distance */ + private double _gentleDescentMins = 0.0, _steepDescentMins; + /** True if parsing from a string failed */ + private boolean _parseFailed = false; + + /** Kilometres unit for comparison */ + private static final Unit KILOMETRES = UnitSetLibrary.UNITS_KILOMETRES; + + + /** + * Bare constructor using default values + */ + public EstimationParameters() + { + resetToDefaults(); + } + + /** + * Constructor from config string + * @param inConfigString single, semicolon-separated string from config + */ + public EstimationParameters(String inConfigString) + { + populateWithString(inConfigString); + if (_parseFailed) { + resetToDefaults(); + } + } + + /** + * Reset all the values to their hardcoded defaults + */ + public void resetToDefaults() + { + _flatMins = 60.0; + _gentleClimbMins = 12.0; _steepClimbMins = 18.0; + _gentleDescentMins = 0.0; _steepDescentMins = 12.0; + _parseFailed = false; + } + + /** + * Populate the values from the config, which means all values are metric + * @param inString semicolon-separated string of five parameters + */ + private void populateWithString(String inString) + { + if (inString != null && !inString.trim().equals("")) + { + String[] params = inString.trim().split(";"); + _parseFailed = (params == null || params.length != 5); + if (!_parseFailed) + { + for (String p : params) + { + if (!isParamStringValid(p)) { + _parseFailed = true; + } + } + } + if (!_parseFailed) + { + try + { + // Use fixed UK locale to parse these, because of fixed "." formatting + NumberFormat twoDpFormatter = NumberFormat.getNumberInstance(Locale.UK); + _flatMins = twoDpFormatter.parse(params[0]).doubleValue(); + _gentleClimbMins = twoDpFormatter.parse(params[1]).doubleValue(); + _steepClimbMins = twoDpFormatter.parse(params[2]).doubleValue(); + _gentleDescentMins = twoDpFormatter.parse(params[3]).doubleValue(); + _steepDescentMins = twoDpFormatter.parse(params[4]).doubleValue(); + } + catch (Exception e) { + _parseFailed = true; + } + } + } + else _parseFailed = true; + } + + /** + * Populate the values using five user-entered strings (now Units-specific!) + * @param inFlat minutes for flat + * @param inGClimb minutes for gentle climb + * @param inSClimb minutes for steep climb + * @param inGDescent minutes for gentle descent + * @param inSDescent minutes for steep descent + */ + public void populateWithStrings(String inFlat, String inGClimb, String inSClimb, String inGDescent, String inSDescent) + { + if (isParamStringValid(inFlat) && isParamStringValid(inGClimb) && isParamStringValid(inSClimb) + && isParamStringValid(inGDescent) && isParamStringValid(inSDescent)) + { + Unit distUnit = Config.getUnitSet().getDistanceUnit(); + Unit altUnit = Config.getUnitSet().getAltitudeUnit(); + final double distFactor = (distUnit == KILOMETRES ? 1.0 : (5000/3.0 * distUnit.getMultFactorFromStd())); + final double altFactor = (altUnit.isStandard() ? 1.0 : (1.0/3.0 * altUnit.getMultFactorFromStd())); + NumberFormat localFormatter = NumberFormat.getNumberInstance(); + try + { + _flatMins = localFormatter.parse(inFlat).doubleValue() * distFactor; + _gentleClimbMins = localFormatter.parse(inGClimb).doubleValue() * altFactor; + _steepClimbMins = localFormatter.parse(inSClimb).doubleValue() * altFactor; + _gentleDescentMins = localFormatter.parse(inGDescent).doubleValue() * altFactor; + _steepDescentMins = localFormatter.parse(inSDescent).doubleValue() * altFactor; + } + catch (Exception e) {_parseFailed = true;} + } + else _parseFailed = true; + } + + /** + * Populate with double metric values, for example the results of a Learning process + * @param inFlat time for 5km on the flat + * @param inGClimb time for 100m gentle climb + * @param inSClimb time for 100m steep climb + * @param inGDescent time for 100m gentle descent + * @param inSDescent time for 100m steep descent + */ + public void populateWithMetrics(double inFlat, double inGClimb, double inSClimb, double inGDescent, double inSDescent) + { + _flatMins = inFlat; + _gentleClimbMins = inGClimb; + _steepClimbMins = inSClimb; + _gentleDescentMins = inGDescent; + _steepDescentMins = inSDescent; + } + + /** + * @param inString parameter string to check + * @return true if it looks valid (no letters, at least one digit) + */ + private static boolean isParamStringValid(String inString) + { + if (inString == null) {return false;} + boolean hasDigit = false, currPunc = false, prevPunc = false; + for (int i=0; i 0.0 && _gentleClimbMins >= 0.0 && _steepClimbMins >= 0.0; + } + + /** + * @return five strings for putting in text fields for editing / display + */ + public String[] getStrings() + { + Unit distUnit = Config.getUnitSet().getDistanceUnit(); + Unit altUnit = Config.getUnitSet().getAltitudeUnit(); + double distFactor = (distUnit == KILOMETRES ? 1.0 : (5000/3.0 * distUnit.getMultFactorFromStd())); + double altFactor = (altUnit.isStandard() ? 1.0 : (1.0/3.0 * altUnit.getMultFactorFromStd())); + // Use locale-specific number formatting, eg commas for german + NumberFormat numFormatter = NumberFormat.getNumberInstance(); + if (numFormatter instanceof DecimalFormat) { + ((DecimalFormat) numFormatter).applyPattern("0.00"); + } + // Conversion between units + return new String[] { + numFormatter.format(_flatMins / distFactor), + numFormatter.format(_gentleClimbMins / altFactor), numFormatter.format(_steepClimbMins / altFactor), + numFormatter.format(_gentleDescentMins / altFactor), numFormatter.format(_steepDescentMins / altFactor) + }; + } + + /** + * @return unit-specific string describing the distance for the flat time (5km/3mi/3NM) + */ + public static String getStandardDistance() + { + Unit distUnit = Config.getUnitSet().getDistanceUnit(); + return (distUnit == KILOMETRES ? "5 " : "3 ") + I18nManager.getText(distUnit.getShortnameKey()); + } + + /** + * @return unit-specific string describing the height difference for the climbs/descents (100m/300ft) + */ + public static String getStandardClimb() + { + Unit altUnit = Config.getUnitSet().getAltitudeUnit(); + return (altUnit.isStandard() ? "100 " : "300 ") + I18nManager.getText(altUnit.getShortnameKey()); + } + + /** + * @return contents of parameters as a semicolon-separated (metric) string for the config + */ + public String toConfigString() + { + return "" + twoDp(_flatMins) + ";" + twoDp(_gentleClimbMins) + ";" + twoDp(_steepClimbMins) + ";" + + twoDp(_gentleDescentMins) + ";" + twoDp(_steepDescentMins); + } + + /** + * @param inNum number to output + * @return formatted string to two decimal places, with decimal point + */ + private static String twoDp(double inNum) + { + if (inNum < 0.0) return "-" + twoDp(-inNum); + int hundreds = (int) (inNum * 100 + 0.5); + return "" + (hundreds / 100) + "." + (hundreds % 100); + } + + /** + * Apply the parameters to the given range stats + * @param inStats stats of current range + * @return estimated number of minutes required + */ + public double applyToStats(RangeStats inStats) + { + if (inStats == null || !inStats.isValid()) return 0.0; + final Unit METRES = UnitSetLibrary.UNITS_METRES; + final double STANDARD_CLIMB = 100.0; // metres + final double STANDARD_DISTANCE = 5.0; // kilometres + return _flatMins * inStats.getMovingDistanceKilometres() / STANDARD_DISTANCE + + _gentleClimbMins * inStats.getGentleAltitudeRange().getClimb(METRES) / STANDARD_CLIMB + + _steepClimbMins * inStats.getSteepAltitudeRange().getClimb(METRES) / STANDARD_CLIMB + + _gentleDescentMins * inStats.getGentleAltitudeRange().getDescent(METRES) / STANDARD_CLIMB + + _steepDescentMins * inStats.getSteepAltitudeRange().getDescent(METRES) / STANDARD_CLIMB; + } + + /** + * Combine two sets of parameters together + * @param inOther other set + * @param inFraction fractional weight + * @return combined set + */ + public EstimationParameters combine(EstimationParameters inOther, double inFraction) + { + if (inFraction < 0.0 || inFraction > 1.0 || inOther == null) { + return null; + } + // inFraction is the weight of this one, weight of the other one is 1-inFraction + final double fraction2 = 1 - inFraction; + EstimationParameters combined = new EstimationParameters(); + combined._flatMins = inFraction * _flatMins + fraction2 * inOther._flatMins; + combined._gentleClimbMins = inFraction * _gentleClimbMins + fraction2 * inOther._gentleClimbMins; + combined._gentleDescentMins = inFraction * _gentleDescentMins + fraction2 * inOther._gentleDescentMins; + combined._steepClimbMins = inFraction * _steepClimbMins + fraction2 * inOther._steepClimbMins; + combined._steepDescentMins = inFraction * _steepDescentMins + fraction2 * inOther._steepDescentMins; + return combined; + } +} diff --git a/tim/prune/function/estimate/LearnParameters.java b/tim/prune/function/estimate/LearnParameters.java new file mode 100644 index 0000000..8f4ea68 --- /dev/null +++ b/tim/prune/function/estimate/LearnParameters.java @@ -0,0 +1,520 @@ +package tim.prune.function.estimate; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.util.ArrayList; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollBar; + +import tim.prune.App; +import tim.prune.GenericFunction; +import tim.prune.I18nManager; +import tim.prune.config.Config; +import tim.prune.data.DataPoint; +import tim.prune.data.Distance; +import tim.prune.data.RangeStats; +import tim.prune.data.Track; +import tim.prune.data.Unit; +import tim.prune.data.UnitSetLibrary; +import tim.prune.function.estimate.jama.Matrix; +import tim.prune.gui.ProgressDialog; + +/** + * Function to learn the estimation parameters from the current track + */ +public class LearnParameters extends GenericFunction implements Runnable +{ + /** Progress dialog */ + ProgressDialog _progress = null; + /** Results dialog */ + JDialog _dialog = null; + /** Calculated parameters */ + private ParametersPanel _calculatedParamPanel = null; + private EstimationParameters _calculatedParams = null; + /** Slider for weighted average */ + private JScrollBar _weightSlider = null; + /** Label to describe position of slider */ + private JLabel _sliderDescLabel = null; + /** Combined parameters */ + private ParametersPanel _combinedParamPanel = null; + /** Combine button */ + private JButton _combineButton = null; + + + /** + * Inner class used to hold the results of the matrix solving + */ + static class MatrixResults + { + public EstimationParameters _parameters = null; + public double _averageErrorPc = 0.0; // percentage + } + + + /** + * Constructor + * @param inApp App object + */ + public LearnParameters(App inApp) + { + super(inApp); + } + + /** @return key for function name */ + public String getNameKey() { + return "function.learnestimationparams"; + } + + /** + * Begin the function + */ + public void begin() + { + // Show progress bar + if (_progress == null) { + _progress = new ProgressDialog(_parentFrame, getNameKey()); + } + _progress.show(); + // Start new thread for the calculations + new Thread(this).start(); + } + + /** + * Run method in separate thread + */ + public void run() + { + _progress.setMaximum(100); + // Go through the track and collect the range stats for each sample + ArrayList statsList = new ArrayList(20); + Track track = _app.getTrackInfo().getTrack(); + final int numPoints = track.getNumPoints(); + final int sampleSize = numPoints / 30; + int prevStartIndex = -1; + for (int i=0; i<30; i++) + { + int startIndex = i * sampleSize; + RangeStats stats = getRangeStats(track, startIndex, startIndex + sampleSize, prevStartIndex); + if (stats != null && stats.getMovingDistanceKilometres() > 1.0 + && !stats.getTimestampsIncomplete() + && stats.getTotalDurationInSeconds() > 100 + && stats.getStartIndex() > prevStartIndex) + { + // System.out.println("Got stats for " + stats.getStartIndex() + " to " + stats.getEndIndex()); + statsList.add(stats); + prevStartIndex = stats.getStartIndex(); + } + _progress.setValue(i); + } + + // Check if we've got enough samples + // System.out.println("Got a total of " + statsList.size() + " samples"); + if (statsList.size() < 10) + { + _progress.dispose(); + // Show error message, not enough samples + _app.showErrorMessage(getNameKey(), "error.learnestimationparams.failed"); + return; + } + // Loop around, solving the matrices and removing the highest-error sample + MatrixResults results = reduceSamples(statsList); + if (results == null) + { + _progress.dispose(); + _app.showErrorMessage(getNameKey(), "error.learnestimationparams.failed"); + return; + } + + _progress.dispose(); + + // Create the dialog if necessary + if (_dialog == null) + { + _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true); + _dialog.setLocationRelativeTo(_parentFrame); + // Create Gui and show it + _dialog.getContentPane().add(makeDialogComponents()); + _dialog.pack(); + } + + // Populate the values in the dialog + populateCalculatedValues(results); + updateCombinedLabels(calculateCombinedParameters()); + _dialog.setVisible(true); + } + + + /** + * Make the dialog components + * @return the GUI components for the dialog + */ + private Component makeDialogComponents() + { + JPanel dialogPanel = new JPanel(); + dialogPanel.setLayout(new BorderLayout()); + + // main panel with a box layout + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); + // Label at top + JLabel introLabel = new JLabel(I18nManager.getText("dialog.learnestimationparams.intro") + ":"); + introLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + introLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + mainPanel.add(introLabel); + + // Panel for the calculated results + _calculatedParamPanel = new ParametersPanel("dialog.estimatetime.results", true); + _calculatedParamPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + mainPanel.add(_calculatedParamPanel); + mainPanel.add(Box.createVerticalStrut(14)); + + mainPanel.add(new JLabel(I18nManager.getText("dialog.learnestimationparams.combine") + ":")); + mainPanel.add(Box.createVerticalStrut(4)); + _weightSlider = new JScrollBar(JScrollBar.HORIZONTAL, 5, 1, 0, 11); + _weightSlider.addAdjustmentListener(new AdjustmentListener() { + public void adjustmentValueChanged(AdjustmentEvent inEvent) + { + if (!inEvent.getValueIsAdjusting()) { + updateCombinedLabels(calculateCombinedParameters()); + } + } + }); + mainPanel.add(_weightSlider); + _sliderDescLabel = new JLabel(" "); + _sliderDescLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + mainPanel.add(_sliderDescLabel); + mainPanel.add(Box.createVerticalStrut(12)); + + // Results panel + _combinedParamPanel = new ParametersPanel("dialog.learnestimationparams.combinedresults"); + _combinedParamPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + mainPanel.add(_combinedParamPanel); + + dialogPanel.add(mainPanel, BorderLayout.NORTH); + + // button panel at bottom + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + + // Combine + _combineButton = new JButton(I18nManager.getText("button.combine")); + _combineButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + combineAndFinish(); + } + }); + buttonPanel.add(_combineButton); + + // Cancel + JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + _dialog.dispose(); + } + }); + KeyAdapter escapeListener = new KeyAdapter() { + public void keyPressed(KeyEvent inE) { + if (inE.getKeyCode() == KeyEvent.VK_ESCAPE) {_dialog.dispose();} + } + }; + _combineButton.addKeyListener(escapeListener); + cancelButton.addKeyListener(escapeListener); + buttonPanel.add(cancelButton); + dialogPanel.add(buttonPanel, BorderLayout.SOUTH); + return dialogPanel; + } + + /** + * Construct a rangestats object for the selected range + * @param inTrack track object + * @param inStartIndex start index + * @param inEndIndex end index + * @param inPreviousStartIndex the previously used start index, or -1 + * @return range stats object or null if required information missing from this bit of the track + */ + private RangeStats getRangeStats(Track inTrack, int inStartIndex, int inEndIndex, int inPreviousStartIndex) + { + // Check parameters + if (inTrack == null || inStartIndex < 0 || inEndIndex <= inStartIndex || inStartIndex > inTrack.getNumPoints()) { + return null; + } + final int numPoints = inTrack.getNumPoints(); + int start = inStartIndex; + + // Search forward until a decent track point found for the start + DataPoint p = inTrack.getPoint(start); + while (start < numPoints && (p == null || p.isWaypoint() || !p.hasTimestamp() || !p.hasAltitude())) + { + start++; + p = inTrack.getPoint(start); + } + if (inPreviousStartIndex >= 0 && start <= (inPreviousStartIndex + 10) // overlapping too much with previous range + || (start >= (numPoints - 10))) // starting too late in the track + { + return null; + } + + // Search forward (counting the radians) until a decent end point found + double movingRads = 0.0; + final double minimumRads = Distance.convertDistanceToRadians(1.0, UnitSetLibrary.UNITS_KILOMETRES); + DataPoint prevPoint = inTrack.getPoint(start); + int endIndex = start; + boolean shouldStop = false; + do + { + endIndex++; + p = inTrack.getPoint(endIndex); + if (p != null && !p.isWaypoint()) + { + if (!p.hasAltitude() || !p.hasTimestamp()) {return null;} // abort if no time/altitude + if (prevPoint != null && !p.getSegmentStart()) { + movingRads += DataPoint.calculateRadiansBetween(prevPoint, p); + } + } + prevPoint = p; + if (endIndex >= numPoints) { + shouldStop = true; // reached the end of the track + } + else if (movingRads >= minimumRads && endIndex >= inEndIndex) { + shouldStop = true; // got at least a kilometre + } + } + while (!shouldStop); + + // Check moving distance + if (movingRads >= minimumRads) { + return new RangeStats(inTrack, start, endIndex); + } + return null; + } + + /** + * Build an A matrix for the given list of RangeStats objects + * @param inStatsList list of (non-null) RangeStats objects + * @return A matrix with n rows and 5 columns + */ + private static Matrix buildAMatrix(ArrayList inStatsList) + { + final Unit METRES = UnitSetLibrary.UNITS_METRES; + Matrix result = new Matrix(inStatsList.size(), 5); + int row = 0; + for (RangeStats stats : inStatsList) + { + result.setValue(row, 0, stats.getMovingDistanceKilometres()); + result.setValue(row, 1, stats.getGentleAltitudeRange().getClimb(METRES)); + result.setValue(row, 2, stats.getSteepAltitudeRange().getClimb(METRES)); + result.setValue(row, 3, stats.getGentleAltitudeRange().getDescent(METRES)); + result.setValue(row, 4, stats.getSteepAltitudeRange().getDescent(METRES)); + row++; + } + return result; + } + + /** + * Build a B matrix containing the observations (moving times) + * @param inStatsList list of (non-null) RangeStats objects + * @return B matrix with single column of n rows + */ + private static Matrix buildBMatrix(ArrayList inStatsList) + { + Matrix result = new Matrix(inStatsList.size(), 1); + int row = 0; + for (RangeStats stats : inStatsList) + { + result.setValue(row, 0, stats.getMovingDurationInSeconds() / 60.0); // convert seconds to minutes + row++; + } + return result; + } + + /** + * Look for the maximum absolute value in the given column matrix + * @param inMatrix matrix with only one column + * @return row index of cell with greatest absolute value, or -1 if not valid + */ + private static int getIndexOfMaxValue(Matrix inMatrix) + { + if (inMatrix == null || inMatrix.getNumColumns() > 1) { + return -1; + } + int index = 0; + double currValue = 0.0, maxValue = 0.0; + // Loop over the first column looking for the maximum absolute value + for (int i=0; i maxValue) + { + maxValue = currValue; + index = i; + } + } + return index; + } + + /** + * See if the given set of samples is sufficient for getting a descent solution (at least 3 nonzero values) + * @param inRangeSet list of RangeStats objects + * @param inRowToIgnore row index to ignore, or -1 to use them all + * @return true if the samples look ok + */ + private static boolean isRangeSetSufficient(ArrayList inRangeSet, int inRowToIgnore) + { + int numGC = 0, numSC = 0, numGD = 0, numSD = 0; // number of samples with gentle/steep climb/descent values > 0 + final Unit METRES = UnitSetLibrary.UNITS_METRES; + int i = 0; + for (RangeStats stats : inRangeSet) + { + if (i != inRowToIgnore) + { + if (stats.getGentleAltitudeRange().getClimb(METRES) > 0) {numGC++;} + if (stats.getSteepAltitudeRange().getClimb(METRES) > 0) {numSC++;} + if (stats.getGentleAltitudeRange().getDescent(METRES) > 0) {numGD++;} + if (stats.getSteepAltitudeRange().getDescent(METRES) > 0) {numSD++;} + } + i++; + } + return numGC > 3 && numSC > 3 && numGD > 3 && numSD > 3; + } + + /** + * Reduce the number of samples in the given list by eliminating the ones with highest errors + * @param inStatsList list of stats + * @return results in an object + */ + private MatrixResults reduceSamples(ArrayList inStatsList) + { + int statsIndexToRemove = -1; + Matrix answer = null; + boolean finished = false; + double averageErrorPc = 0.0; + while (!finished) + { + // Remove the marked stats object, if any + if (statsIndexToRemove >= 0) { + inStatsList.remove(statsIndexToRemove); + } + + // Build up the matrices + Matrix A = buildAMatrix(inStatsList); + Matrix B = buildBMatrix(inStatsList); + // System.out.println("Times in minutes are:\n" + B.toString()); + + // Solve (if possible) + try + { + answer = A.solve(B); + // System.out.println("Solved matrix with " + A.getNumRows() + " rows:\n" + answer.toString()); + // Work out the percentage error for each estimate + Matrix estimates = A.times(answer); + Matrix errors = estimates.minus(B).divideEach(B); + // System.out.println("Errors: " + errors.toString()); + averageErrorPc = errors.getAverageAbsValue(); + // find biggest percentage error, remove it from list + statsIndexToRemove = getIndexOfMaxValue(errors); + if (statsIndexToRemove < 0) + { + System.err.println("Something wrong - index is " + statsIndexToRemove); + throw new Exception(); + } + // Check whether removing this element would make the range set insufficient + finished = inStatsList.size() <= 25 || !isRangeSetSufficient(inStatsList, statsIndexToRemove); + } + catch (Exception e) + { + // Couldn't solve at all + System.out.println("Failed to reduce: " + e.getClass().getName() + " - " + e.getMessage()); + return null; + } + _progress.setValue(20 + 80 * (30 - inStatsList.size())/5); // Counting from 30 to 25 + } + // Copy results to an EstimationParameters object + MatrixResults result = new MatrixResults(); + result._parameters = new EstimationParameters(); + result._parameters.populateWithMetrics(answer.get(0, 0) * 5, // convert from 1km to 5km + answer.get(1, 0) * 100.0, answer.get(2, 0) * 100.0, // convert from m to 100m + answer.get(3, 0) * 100.0, answer.get(4, 0) * 100.0); + result._averageErrorPc = averageErrorPc; + return result; + } + + + /** + * Populate the dialog's labels with the calculated values + * @param inResults results of the calculations + */ + private void populateCalculatedValues(MatrixResults inResults) + { + if (inResults == null || inResults._parameters == null) + { + _calculatedParams = null; + _calculatedParamPanel.updateParameters(null, 0.0); + } + else + { + _calculatedParams = inResults._parameters; + _calculatedParamPanel.updateParameters(_calculatedParams, inResults._averageErrorPc); + } + } + + /** + * Combine the calculated parameters with the existing ones + * according to the value of the slider + * @return combined parameters + */ + private EstimationParameters calculateCombinedParameters() + { + final double fraction1 = 1 - 0.1 * _weightSlider.getValue(); // slider left = value 0 = fraction 1 = keep current + EstimationParameters oldParams = new EstimationParameters(Config.getConfigString(Config.KEY_ESTIMATION_PARAMS)); + return oldParams.combine(_calculatedParams, fraction1); + } + + /** + * Update the labels to show the combined parameters + * @param inCombinedParams combined estimation parameters + */ + private void updateCombinedLabels(EstimationParameters inCombinedParams) + { + // Update the slider description label + String sliderDesc = null; + final int sliderVal = _weightSlider.getValue(); + switch (sliderVal) + { + case 0: sliderDesc = I18nManager.getText("dialog.learnestimationparams.weight.100pccurrent"); break; + case 5: sliderDesc = I18nManager.getText("dialog.learnestimationparams.weight.50pc"); break; + case 10: sliderDesc = I18nManager.getText("dialog.learnestimationparams.weight.100pccalculated"); break; + default: + final int currTenths = 10 - sliderVal, calcTenths = sliderVal; + sliderDesc = "" + currTenths + "0% " + I18nManager.getText("dialog.learnestimationparams.weight.current") + + " + " + calcTenths + "0% " + I18nManager.getText("dialog.learnestimationparams.weight.calculated"); + } + _sliderDescLabel.setText(sliderDesc); + // And update all the combined params labels + _combinedParamPanel.updateParameters(inCombinedParams); + _combineButton.setEnabled(sliderVal > 0); + } + + /** + * React to the combine button, by saving the combined parameters in the config + */ + private void combineAndFinish() + { + EstimationParameters params = calculateCombinedParameters(); + Config.setConfigString(Config.KEY_ESTIMATION_PARAMS, params.toConfigString()); + _dialog.dispose(); + } +} diff --git a/tim/prune/function/estimate/ParametersPanel.java b/tim/prune/function/estimate/ParametersPanel.java new file mode 100644 index 0000000..3632831 --- /dev/null +++ b/tim/prune/function/estimate/ParametersPanel.java @@ -0,0 +1,158 @@ +package tim.prune.function.estimate; + +import java.awt.Component; +import java.awt.GridLayout; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import tim.prune.I18nManager; +import tim.prune.gui.DisplayUtils; + +/** + * Display panel for showing estimation parameters + * in a standard grid form + */ +public class ParametersPanel extends JPanel +{ + /** Flag for whether average error should be shown */ + private boolean _showAverageError = false; + /** Labels for calculated parameters */ + private JLabel _fsUnitsLabel = null, _flatSpeedLabel = null; + private JLabel _climbUnitsLabel = null; + private JLabel _gentleClimbLabel = null, _steepClimbLabel = null; + private JLabel _descentUnitsLabel = null; + private JLabel _gentleDescentLabel = null, _steepDescentLabel = null; + private JLabel _averageErrorLabel = null; + + + /** + * Constructor + * @param inTitleKey key to use for title of panel + */ + public ParametersPanel(String inTitleKey) + { + this(inTitleKey, false); + } + + /** + * Constructor + * @param inTitleKey key to use for title of panel + * @param inShowAvgError true to show average error line + */ + public ParametersPanel(String inTitleKey, boolean inShowAvgError) + { + super(); + _showAverageError = inShowAvgError; + if (inTitleKey != null) { + setBorder(BorderFactory.createTitledBorder(I18nManager.getText(inTitleKey))); + } + setLayout(new GridLayout(0, 3, 3, 3)); + addLabels(); + } + + private void addLabels() + { + // flat speed + _fsUnitsLabel = new JLabel(I18nManager.getText("dialog.estimatetime.parameters.timefor") + " 5km : "); + _fsUnitsLabel.setAlignmentX(Component.RIGHT_ALIGNMENT); + add(_fsUnitsLabel); + _flatSpeedLabel = new JLabel("60 minutes"); // (filled in later) + add(_flatSpeedLabel); + add(new JLabel("")); + // Headers for gentle and steep + add(new JLabel("")); + JLabel gentleLabel = new JLabel(I18nManager.getText("dialog.estimatetime.gentle")); + add(gentleLabel); + JLabel steepLabel = new JLabel(I18nManager.getText("dialog.estimatetime.steep")); + add(steepLabel); + // Climb + _climbUnitsLabel = new JLabel("Climb 100m: "); + _climbUnitsLabel.setAlignmentX(Component.RIGHT_ALIGNMENT); + add(_climbUnitsLabel); + _gentleClimbLabel = new JLabel("22 minutes"); // (filled in later) + add(_gentleClimbLabel); + _steepClimbLabel = new JLabel("22 minutes"); // (filled in later) + add(_steepClimbLabel); + // Descent + _descentUnitsLabel = new JLabel(I18nManager.getText("dialog.estimatetime.parameters.timefor") + ": "); + _descentUnitsLabel.setAlignmentX(Component.RIGHT_ALIGNMENT); + add(_descentUnitsLabel); + _gentleDescentLabel = new JLabel("22 minutes"); // (filled in later) + add(_gentleDescentLabel); + _steepDescentLabel = new JLabel("22 minutes"); // (filled in later) + add(_steepDescentLabel); + // Average error + if (_showAverageError) + { + JLabel errorLabel = new JLabel(I18nManager.getText("dialog.learnestimationparams.averageerror") + ": "); + errorLabel.setAlignmentX(Component.RIGHT_ALIGNMENT); + add(errorLabel); + _averageErrorLabel = new JLabel("22 minutes"); // (filled in later) + add(_averageErrorLabel); + } + } + + /** + * Update the labels using the given parameters + * @param inParams the parameters used or calculated + * @param inAverageError average error as percentage + * @param inShowError true to show this error value, false otherwise + */ + private void updateParameters(EstimationParameters inParams, double inAverageError, boolean inShowError) + { + if (inParams == null || !inParams.isValid()) + { + _flatSpeedLabel.setText(""); + _gentleClimbLabel.setText(""); _steepClimbLabel.setText(""); + _gentleDescentLabel.setText(""); _steepDescentLabel.setText(""); + } + else + { + final String minsText = " " + I18nManager.getText("units.minutes"); + String[] values = inParams.getStrings(); // these strings are already formatted locally + _fsUnitsLabel.setText(I18nManager.getText("dialog.estimatetime.parameters.timefor") + + " " + EstimationParameters.getStandardDistance() + ": "); + _flatSpeedLabel.setText(values[0] + minsText); + final String heightString = " " + EstimationParameters.getStandardClimb() + ": "; + _climbUnitsLabel.setText(I18nManager.getText("dialog.estimatetime.climb") + heightString); + _gentleClimbLabel.setText(values[1] + minsText); + _steepClimbLabel.setText(values[2] + minsText); + _descentUnitsLabel.setText(I18nManager.getText("dialog.estimatetime.descent") + heightString); + _gentleDescentLabel.setText(values[3] + minsText); + _steepDescentLabel.setText(values[4] + minsText); + } + // Average error + if (_averageErrorLabel != null) + { + if (inParams == null || !inParams.isValid() || !inShowError) + { + _averageErrorLabel.setText(""); + } + else + { + _averageErrorLabel.setText(DisplayUtils.formatOneDp(inAverageError) + " %"); + } + } + } + + /** + * Just show the parameters, with no average error + * @param inParams parameters to show + */ + public void updateParameters(EstimationParameters inParams) + { + updateParameters(inParams, 0.0, false); + } + + /** + * Show the parameters and the average error + * @param inParams parameters to show + * @param inAverageError average error as percentage + */ + public void updateParameters(EstimationParameters inParams, double inAverageError) + { + updateParameters(inParams, inAverageError, true); + } +} diff --git a/tim/prune/function/estimate/jama/Maths.java b/tim/prune/function/estimate/jama/Maths.java new file mode 100644 index 0000000..7c3326d --- /dev/null +++ b/tim/prune/function/estimate/jama/Maths.java @@ -0,0 +1,31 @@ +package tim.prune.function.estimate.jama; + +/** + * Static helper method, taken from public domain NIST code for JAMA + */ +public abstract class Maths +{ + /** + * Work out sqrt(a^2 + b^2) + */ + public static double pythag(double a, double b) + { + double r; + + if (Math.abs(a) > Math.abs(b)) + { + r = b/a; + r = Math.abs(a)*Math.sqrt(1+r*r); + } + else if (b != 0) + { + r = a/b; + r = Math.abs(b)*Math.sqrt(1+r*r); + } + else + { + r = 0.0; + } + return r; + } +} diff --git a/tim/prune/function/estimate/jama/Matrix.java b/tim/prune/function/estimate/jama/Matrix.java new file mode 100644 index 0000000..face1a9 --- /dev/null +++ b/tim/prune/function/estimate/jama/Matrix.java @@ -0,0 +1,259 @@ +package tim.prune.function.estimate.jama; + +/** + * The Java Matrix Class provides the fundamental operations of numerical linear algebra. + * Original authors The MathWorks, Inc. and the National Institute of Standards and Technology + * The original public domain code has now been modified and reduced to only contain + * the use of QR Decomposition of rectangular matrices, to solve least squares regression, + * and is placed under GPL2 with the rest of the GpsPrune code. + */ +public class Matrix +{ + + /** Array for internal storage of elements */ + private double[][] _matrix; + + /** Row and column dimensions */ + private int _m, _n; + + + /** + * Construct an m-by-n matrix of zeros + * @param inM Number of rows + * @param inN Number of colums + */ + public Matrix(int inM, int inN) + { + _m = inM; + _n = inN; + _matrix = new double[inM][inN]; + } + + /** + * Construct a matrix from a 2-D array + * @param A Two-dimensional array of doubles. + * @exception IllegalArgumentException All rows must have the same length + */ + public Matrix(double[][] A) + { + _m = A.length; + _n = A[0].length; + for (int i = 0; i < _m; i++) { + if (A[i].length != _n) { + throw new IllegalArgumentException("All rows must have the same length."); + } + } + _matrix = A; + } + + /** + * Construct a matrix quickly without checking arguments. + * @param inA Two-dimensional array of doubles. + * @param inM Number of rows + * @param inN Number of columns + */ + public Matrix(double[][] inA, int inM, int inN) + { + _matrix = inA; + _m = inM; + _n = inN; + } + + /* + * ------------------------ Public Methods ------------------------ + */ + + + /** + * Set a value in a cell of the matrix + * @param inRow row index + * @param inCol column index + * @param inValue value to set + */ + public void setValue(int inRow, int inCol, double inValue) + { + _matrix[inRow][inCol] = inValue; + } + + /** + * Access the internal two-dimensional array. + * @return Pointer to the two-dimensional array of matrix elements. + */ + public double[][] getArray() { + return _matrix; + } + + /** + * Copy the internal two-dimensional array. + * @return Two-dimensional array copy of matrix elements. + */ + public double[][] getArrayCopy() + { + double[][] C = new double[_m][_n]; + for (int i = 0; i < _m; i++) { + for (int j = 0; j < _n; j++) { + C[i][j] = _matrix[i][j]; + } + } + return C; + } + + /** + * Get a single element. + * @param inRow Row index + * @param inCol Column index + * @return A(inRow,inCol) + * @exception ArrayIndexOutOfBoundsException + */ + public double get(int inRow, int inCol) { + return _matrix[inRow][inCol]; + } + + /** @return number of rows _m */ + public int getNumRows() { + return _m; + } + + /** @return number of columns _n */ + public int getNumColumns() { + return _n; + } + + /** + * Get a submatrix + * @param i0 Initial row index + * @param i1 Final row index + * @param j0 Initial column index + * @param j1 Final column index + * @return A(i0:i1,j0:j1) + * @exception ArrayIndexOutOfBoundsException + */ + public Matrix getMatrix(int i0, int i1, int j0, int j1) + { + Matrix X = new Matrix(i1 - i0 + 1, j1 - j0 + 1); + double[][] B = X.getArray(); + try { + for (int i = i0; i <= i1; i++) { + for (int j = j0; j <= j1; j++) { + B[i - i0][j - j0] = _matrix[i][j]; + } + } + } catch (ArrayIndexOutOfBoundsException e) { + throw new ArrayIndexOutOfBoundsException("Submatrix indices"); + } + return X; + } + + + /** + * Linear algebraic matrix multiplication, A * B + * @param B another matrix + * @return Matrix product, A * B + * @exception IllegalArgumentException if matrix dimensions don't agree + */ + public Matrix times(Matrix B) + { + if (B._m != _n) { + throw new IllegalArgumentException("Matrix inner dimensions must agree."); + } + Matrix X = new Matrix(_m, B._n); + double[][] C = X.getArray(); + double[] Bcolj = new double[_n]; + for (int j = 0; j < B._n; j++) { + for (int k = 0; k < _n; k++) { + Bcolj[k] = B._matrix[k][j]; + } + for (int i = 0; i < _m; i++) { + double[] Arowi = _matrix[i]; + double s = 0; + for (int k = 0; k < _n; k++) { + s += Arowi[k] * Bcolj[k]; + } + C[i][j] = s; + } + } + return X; + } + + /** + * Subtract the other matrix from this one + * @param B another matrix + * @return difference this - B + * @exception IllegalArgumentException if matrix dimensions don't agree + */ + public Matrix minus(Matrix B) + { + if (B._m != _m || B._n != _n) { + throw new IllegalArgumentException("Matrix dimensions must agree."); + } + Matrix result = new Matrix(_m, _n); + for (int i = 0; i < _m; i++) { + for (int j = 0; j < _n; j++) { + result.setValue(i, j, get(i, j) - B.get(i, j)); + } + } + return result; + } + + /** + * Divide each element of this matrix by the corresponding element in the other one + * @param B another matrix + * @return this[i,j]/other[i,j] + * @exception IllegalArgumentException if matrix dimensions don't agree + */ + public Matrix divideEach(Matrix B) + { + if (B._m != _m || B._n != _n) { + throw new IllegalArgumentException("Matrix dimensions must agree."); + } + Matrix result = new Matrix(_m, _n); + for (int i = 0; i < _m; i++) { + for (int j = 0; j < _n; j++) { + result.setValue(i, j, get(i, j) / B.get(i, j)); + } + } + return result; + } + + /** + * Solve A*X = B + * @param B right hand side + * @return least squares solution + */ + public Matrix solve(Matrix B) { + return new QRDecomposition(this).solve(B); + } + + /** + * @return the average absolute value of all the elements in the matrix + */ + public double getAverageAbsValue() + { + double total = 0.0; + for (int i = 0; i < _m; i++) { + for (int j = 0; j < _n; j++) { + total += Math.abs(_matrix[i][j]); + } + } + return total / _m / _n; + } + + /** + * Primitive output for debugging + */ + public String toString() + { + StringBuilder builder = new StringBuilder(); + builder.append('('); + for (int i = 0; i < _m; i++) { + builder.append('('); + for (int j = 0; j < _n; j++) { + builder.append((_matrix[i][j])); + builder.append(", "); + } + builder.append(") "); + } + builder.append(')'); + return builder.toString(); + } +} diff --git a/tim/prune/function/estimate/jama/QRDecomposition.java b/tim/prune/function/estimate/jama/QRDecomposition.java new file mode 100644 index 0000000..e8aa2b7 --- /dev/null +++ b/tim/prune/function/estimate/jama/QRDecomposition.java @@ -0,0 +1,219 @@ +package tim.prune.function.estimate.jama; + +/** + * QR Decomposition. + * + * For an m-by-n matrix A with m >= n, the QR decomposition is an m-by-n + * orthogonal matrix Q and an n-by-n upper triangular matrix R so that A = Q*R. + * + * The QR decomposition always exists, even if the matrix does not have full + * rank, so the constructor will never fail. The primary use of the QR + * decomposition is in the least squares solution of nonsquare systems of + * simultaneous linear equations. This will fail if isFullRank() returns false. + * + * Original authors The MathWorks, Inc. and the National Institute of Standards and Technology + * The original public domain code has now been modified and reduced, + * and is placed under GPL2 with the rest of the GpsPrune code. + */ +public class QRDecomposition +{ + + /** Array for internal storage of decomposition */ + private double[][] _QR; + + /** Row and column dimensions */ + private int _m, _n; + + /** Array for internal storage of diagonal of R */ + private double[] _Rdiag; + + + /** + * QR Decomposition, computed by Householder reflections. + * + * @param inA Rectangular matrix + * @return Structure to access R and the Householder vectors and compute Q. + */ + public QRDecomposition(Matrix inA) + { + // Initialize. + _QR = inA.getArrayCopy(); + _m = inA.getNumRows(); + _n = inA.getNumColumns(); + _Rdiag = new double[_n]; + + // Main loop. + for (int k = 0; k < _n; k++) + { + // Compute 2-norm of k-th column without under/overflow. + double nrm = 0; + for (int i = k; i < _m; i++) { + nrm = Maths.pythag(nrm, _QR[i][k]); + } + + if (nrm != 0.0) + { + // Form k-th Householder vector. + if (_QR[k][k] < 0) { + nrm = -nrm; + } + for (int i = k; i < _m; i++) { + _QR[i][k] /= nrm; + } + _QR[k][k] += 1.0; + + // Apply transformation to remaining columns. + for (int j = k + 1; j < _n; j++) + { + double s = 0.0; + for (int i = k; i < _m; i++) { + s += _QR[i][k] * _QR[i][j]; + } + s = -s / _QR[k][k]; + for (int i = k; i < _m; i++) { + _QR[i][j] += s * _QR[i][k]; + } + } + } + _Rdiag[k] = -nrm; + } + } + + /* + * ------------------------ Public Methods ------------------------ + */ + + /** + * Is the matrix full rank? + * @return true if R, and hence A, has full rank. + */ + public boolean isFullRank() + { + for (int j = 0; j < _n; j++) { + if (_Rdiag[j] == 0) + return false; + } + return true; + } + + /** + * Return the Householder vectors + * @deprecated + * @return Lower trapezoidal matrix whose columns define the reflections + */ + private Matrix getH() + { + Matrix X = new Matrix(_m, _n); + double[][] H = X.getArray(); + for (int i = 0; i < _m; i++) { + for (int j = 0; j < _n; j++) { + if (i >= j) { + H[i][j] = _QR[i][j]; + } else { + H[i][j] = 0.0; + } + } + } + return X; + } + + /** + * Return the upper triangular factor + * @deprecated + * @return R + */ + private Matrix getR() + { + Matrix X = new Matrix(_n, _n); + double[][] R = X.getArray(); + for (int i = 0; i < _n; i++) { + for (int j = 0; j < _n; j++) { + if (i < j) { + R[i][j] = _QR[i][j]; + } else if (i == j) { + R[i][j] = _Rdiag[i]; + } else { + R[i][j] = 0.0; + } + } + } + return X; + } + + /** + * Generate and return the (economy-sized) orthogonal factor + * @deprecated + * @return Q + */ + private Matrix getQ() + { + Matrix X = new Matrix(_m, _n); + double[][] Q = X.getArray(); + for (int k = _n - 1; k >= 0; k--) { + for (int i = 0; i < _m; i++) { + Q[i][k] = 0.0; + } + Q[k][k] = 1.0; + for (int j = k; j < _n; j++) { + if (_QR[k][k] != 0) { + double s = 0.0; + for (int i = k; i < _m; i++) { + s += _QR[i][k] * Q[i][j]; + } + s = -s / _QR[k][k]; + for (int i = k; i < _m; i++) { + Q[i][j] += s * _QR[i][k]; + } + } + } + } + return X; + } + + /** + * Least squares solution of A*X = B + * @param B A Matrix with as many rows as A and any number of columns + * @return X that minimizes the two norm of Q*R*X-B + * @exception IllegalArgumentException if matrix dimensions don't agree + * @exception RuntimeException if Matrix is rank deficient. + */ + public Matrix solve(Matrix B) + { + if (B.getNumRows() != _m) { + throw new IllegalArgumentException("Matrix row dimensions must agree."); + } + if (!isFullRank()) { + throw new RuntimeException("Matrix is rank deficient."); + } + + // Copy right hand side + int nx = B.getNumColumns(); + double[][] X = B.getArrayCopy(); + + // Compute Y = transpose(Q)*B + for (int k = 0; k < _n; k++) { + for (int j = 0; j < nx; j++) { + double s = 0.0; + for (int i = k; i < _m; i++) { + s += _QR[i][k] * X[i][j]; + } + s = -s / _QR[k][k]; + for (int i = k; i < _m; i++) { + X[i][j] += s * _QR[i][k]; + } + } + } + // Solve R*X = Y; + for (int k = _n - 1; k >= 0; k--) { + for (int j = 0; j < nx; j++) { + X[k][j] /= _Rdiag[k]; + } + for (int i = 0; i < k; i++) { + for (int j = 0; j < nx; j++) { + X[i][j] -= X[k][j] * _QR[i][k]; + } + } + } + return (new Matrix(X, _n, nx).getMatrix(0, _n - 1, 0, nx - 1)); + } +} diff --git a/tim/prune/function/gpsies/GenericDownloaderFunction.java b/tim/prune/function/gpsies/GenericDownloaderFunction.java index cf634c0..286ba1a 100644 --- a/tim/prune/function/gpsies/GenericDownloaderFunction.java +++ b/tim/prune/function/gpsies/GenericDownloaderFunction.java @@ -40,6 +40,8 @@ public abstract class GenericDownloaderFunction extends GenericFunction implemen protected JTable _trackTable = null; /** Cancelled flag */ protected boolean _cancelled = false; + /** error message */ + protected String _errorMessage = null; /** Status label */ protected JLabel _statusLabel = null; /** Description box */ @@ -84,6 +86,7 @@ public abstract class GenericDownloaderFunction extends GenericFunction implemen _showButton.setEnabled(false); _cancelled = false; _descriptionBox.setText(""); + _errorMessage = null; // Start new thread to load list asynchronously new Thread(this).start(); diff --git a/tim/prune/function/gpsies/TrackListModel.java b/tim/prune/function/gpsies/TrackListModel.java index 904dd8b..274c0b8 100644 --- a/tim/prune/function/gpsies/TrackListModel.java +++ b/tim/prune/function/gpsies/TrackListModel.java @@ -57,6 +57,12 @@ public class TrackListModel extends AbstractTableModel return _trackList.size(); } + /** @return true if there are no rows */ + public boolean isEmpty() + { + return getRowCount() == 0; + } + /** * @param inColNum column number * @return column label for given column diff --git a/tim/prune/function/gpsies/UploadGpsiesFunction.java b/tim/prune/function/gpsies/UploadGpsiesFunction.java index 586cbb6..9a4ae30 100644 --- a/tim/prune/function/gpsies/UploadGpsiesFunction.java +++ b/tim/prune/function/gpsies/UploadGpsiesFunction.java @@ -182,7 +182,8 @@ public class UploadGpsiesFunction extends GenericFunction enableOK(); } }; - GuiGridLayout actGrid = new GuiGridLayout(activityPanel, true); + // Why not a simple grid layout here? + GuiGridLayout actGrid = new GuiGridLayout(activityPanel, new double[] {1.0, 1.0}, new boolean[] {false, false}); final int numActivities = ACTIVITY_KEYS.length; _activityCheckboxes = new JCheckBox[numActivities]; for (int i=0; i 0) { // Inform app including undo information diff --git a/tim/prune/function/srtm/TileFinder.java b/tim/prune/function/srtm/TileFinder.java index 209db7f..afcc28f 100644 --- a/tim/prune/function/srtm/TileFinder.java +++ b/tim/prune/function/srtm/TileFinder.java @@ -54,10 +54,11 @@ public abstract class TileFinder */ private static byte[] readDatFile() { + InputStream in = null; try { // Need absolute path to dat file - InputStream in = TileFinder.class.getResourceAsStream("/tim/prune/function/srtm/srtmtiles.dat"); + in = TileFinder.class.getResourceAsStream("/tim/prune/function/srtm/srtmtiles.dat"); if (in != null) { byte[] buffer = new byte[in.available()]; @@ -69,6 +70,13 @@ public abstract class TileFinder catch (java.io.IOException e) { System.err.println("Exception trying to read srtmtiles.dat : " + e.getMessage()); } + finally + { + try { + in.close(); + } + catch (Exception e) {} // ignore + } return null; } } diff --git a/tim/prune/gui/DecimalNumberField.java b/tim/prune/gui/DecimalNumberField.java new file mode 100644 index 0000000..f1baa2a --- /dev/null +++ b/tim/prune/gui/DecimalNumberField.java @@ -0,0 +1,111 @@ +package tim.prune.gui; + +import java.awt.Dimension; + +import javax.swing.JTextField; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.PlainDocument; + +/** + * text field for holding a decimal number with validation + * - doesn't allow certain characters such as a-z to be entered + */ +public class DecimalNumberField extends JTextField +{ + /** + * Inner class to act as document for validation + */ + protected static class DecimalNumberDocument extends PlainDocument + { + private boolean _allowNegative = false; + + /** constructor */ + DecimalNumberDocument(boolean inAllowNegative) { + _allowNegative = inAllowNegative; + } + + /** + * Override the insert string method + * @param offs offset + * @param str string + * @param a attributes + * @throws BadLocationException on insert failure + */ + public void insertString(int offs, String str, AttributeSet a) + throws BadLocationException + { + char[] source = str.toCharArray(); + char[] result = new char[source.length]; + int j = 0; + for (int i = 0; i < result.length; i++) { + if (!Character.isLetter(source[i]) && (_allowNegative || source[i] != '-') && source[i] != ' ') // no letters, no minus sign or space + result[j++] = source[i]; + } + super.insertString(offs, new String(result, 0, j), a); + } + } + + + /** + * Constructor + */ + public DecimalNumberField() + { + super(6); + setDocument(new DecimalNumberDocument(false)); + } + + /** + * Constructor + * @param inAllowNegative true to allow negative numbers + */ + public DecimalNumberField(boolean inAllowNegative) + { + super(6); + setDocument(new DecimalNumberDocument(inAllowNegative)); + } + + /** + * @return double value + */ + public double getValue() + { + return parseValue(getText()); + } + + /** + * @param inValue value to set + */ + public void setValue(double inValue) + { + setText("" + inValue); + } + + /** + * @param inText text to parse + * @return value as double + */ + private static double parseValue(String inText) + { + double value = 0.0; + try { + value = Double.parseDouble(inText); + } + catch (NumberFormatException nfe) {} + if (value < 0) { + value = 0; + } + return value; + } + + /** + * Put a minimum on the minimum width + */ + public Dimension getMinimumSize() + { + Dimension dim = super.getMinimumSize(); + if (dim.width < 50) dim.width = 50; + return dim; + } +} diff --git a/tim/prune/gui/DetailsDisplay.java b/tim/prune/gui/DetailsDisplay.java index 7b3a4ff..13df7cd 100644 --- a/tim/prune/gui/DetailsDisplay.java +++ b/tim/prune/gui/DetailsDisplay.java @@ -8,7 +8,6 @@ import java.awt.Font; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.text.NumberFormat; import javax.swing.BorderFactory; import javax.swing.Box; @@ -51,6 +50,7 @@ public class DetailsDisplay extends GenericDisplay private JLabel _latLabel = null, _longLabel = null; private JLabel _altLabel = null; private JLabel _timeLabel = null; + private JLabel _descLabel = null; private JLabel _speedLabel = null, _vSpeedLabel = null; private JLabel _nameLabel = null, _typeLabel = null; @@ -84,8 +84,6 @@ public class DetailsDisplay extends GenericDisplay // Units private JComboBox _coordFormatDropdown = null; private JComboBox _distUnitsDropdown = null; - // Formatter - private NumberFormat _distanceFormatter = NumberFormat.getInstance(); // Cached labels private static final String LABEL_POINT_SELECTED = I18nManager.getText("details.index.selected") + ": "; @@ -95,6 +93,7 @@ public class DetailsDisplay extends GenericDisplay private static final String LABEL_POINT_TIMESTAMP = I18nManager.getText("fieldname.timestamp") + ": "; private static final String LABEL_POINT_WAYPOINTNAME = I18nManager.getText("fieldname.waypointname") + ": "; private static final String LABEL_POINT_WAYPOINTTYPE = I18nManager.getText("fieldname.waypointtype") + ": "; + private static final String LABEL_POINT_DESCRIPTION = I18nManager.getText("fieldname.description") + ": "; private static final String LABEL_POINT_SPEED = I18nManager.getText("fieldname.speed") + ": "; private static final String LABEL_POINT_VERTSPEED = I18nManager.getText("fieldname.verticalspeed") + ": "; private static final String LABEL_RANGE_SELECTED = I18nManager.getText("details.range.selected") + ": "; @@ -135,6 +134,8 @@ public class DetailsDisplay extends GenericDisplay _timeLabel = new JLabel(""); _timeLabel.setMinimumSize(new Dimension(120, 10)); pointDetailsPanel.add(_timeLabel); + _descLabel = new JLabel(""); + pointDetailsPanel.add(_descLabel); _speedLabel = new JLabel(""); pointDetailsPanel.add(_speedLabel); _vSpeedLabel = new JLabel(""); @@ -296,6 +297,7 @@ public class DetailsDisplay extends GenericDisplay _longLabel.setText(""); _altLabel.setText(""); _timeLabel.setText(""); + _descLabel.setText(""); _nameLabel.setText(""); _typeLabel.setText(""); _speedLabel.setText(""); @@ -315,9 +317,27 @@ public class DetailsDisplay extends GenericDisplay : ""); if (currentPoint.hasTimestamp()) { _timeLabel.setText(LABEL_POINT_TIMESTAMP + currentPoint.getTimestamp().getText()); + _timeLabel.setToolTipText(currentPoint.getTimestamp().getText()); } else { _timeLabel.setText(""); + _timeLabel.setToolTipText(""); + } + // Maybe the point has a description? + String pointDesc = currentPoint.getFieldValue(Field.DESCRIPTION); + if (pointDesc == null || pointDesc.equals("") || currentPoint.hasMedia()) { + _descLabel.setText(""); + _descLabel.setToolTipText(""); + } + else + { + if (pointDesc.length() < 5) { + _descLabel.setText(LABEL_POINT_DESCRIPTION + pointDesc); + } + else { + _descLabel.setText(shortenString(pointDesc)); + } + _descLabel.setToolTipText(pointDesc); } // Speed can come from either timestamps and distances, or speed values in data @@ -325,7 +345,7 @@ public class DetailsDisplay extends GenericDisplay SpeedCalculator.calculateSpeed(_track, currentPointIndex, speedValue); if (speedValue.isValid()) { - String speed = roundedNumber(speedValue.getValue()) + " " + speedUnitsStr; + String speed = DisplayUtils.roundedNumber(speedValue.getValue()) + " " + speedUnitsStr; _speedLabel.setText(LABEL_POINT_SPEED + speed); } else { @@ -337,7 +357,7 @@ public class DetailsDisplay extends GenericDisplay if (speedValue.isValid()) { String vSpeedUnitsStr = I18nManager.getText(unitSet.getVerticalSpeedUnit().getShortnameKey()); - String speed = roundedNumber(speedValue.getValue()) + " " + vSpeedUnitsStr; + String speed = DisplayUtils.roundedNumber(speedValue.getValue()) + " " + vSpeedUnitsStr; _vSpeedLabel.setText(LABEL_POINT_VERTSPEED + speed); } else { @@ -374,12 +394,12 @@ public class DetailsDisplay extends GenericDisplay _rangeLabel.setText(LABEL_RANGE_SELECTED + (selection.getStart()+1) + " " + I18nManager.getText("details.range.to") + " " + (selection.getEnd()+1)); - _distanceLabel.setText(LABEL_RANGE_DISTANCE + roundedNumber(selection.getDistance()) + " " + distUnitsStr); + _distanceLabel.setText(LABEL_RANGE_DISTANCE + DisplayUtils.roundedNumber(selection.getDistance()) + " " + distUnitsStr); if (selection.getNumSeconds() > 0) { _durationLabel.setText(LABEL_RANGE_DURATION + DisplayUtils.buildDurationString(selection.getNumSeconds())); _aveSpeedLabel.setText(I18nManager.getText("details.range.avespeed") + ": " - + roundedNumber(selection.getDistance()/selection.getNumSeconds()*3600.0) + " " + speedUnitsStr); + + DisplayUtils.roundedNumber(selection.getDistance()/selection.getNumSeconds()*3600.0) + " " + speedUnitsStr); } else { _durationLabel.setText(""); @@ -502,27 +522,6 @@ public class DetailsDisplay extends GenericDisplay } - /** - * Format a number to a sensible precision - * @param inDist distance - * @return formatted String - */ - private String roundedNumber(double inDist) - { - // Set precision of formatter - int numDigits = 0; - if (inDist < 1.0) - numDigits = 3; - else if (inDist < 10.0) - numDigits = 2; - else if (inDist < 100.0) - numDigits = 1; - // set formatter - _distanceFormatter.setMaximumFractionDigits(numDigits); - _distanceFormatter.setMinimumFractionDigits(numDigits); - return _distanceFormatter.format(inDist); - } - /** * Restrict the given coordinate to a limited number of decimal places for display * @param inCoord coordinate string @@ -582,15 +581,26 @@ public class DetailsDisplay extends GenericDisplay */ private static String shortenPath(String inFullPath) { + String path = inFullPath; // Chop off the home path if possible final String homePath = System.getProperty("user.home").toLowerCase(); if (inFullPath != null && inFullPath.toLowerCase().startsWith(homePath)) { - inFullPath = inFullPath.substring(homePath.length()+1); + path = inFullPath.substring(homePath.length()+1); } - if (inFullPath == null || inFullPath.length() < 21) { - return inFullPath; + return shortenString(path); + } + + /** + * @param inString string to shorten + * @return shortened string from the beginning + */ + private static String shortenString(String inString) + { + // Limit is hardcoded here, maybe it should depend on parent component width and font size etc? + if (inString == null || inString.length() < 21) { + return inString; } - // path is too long - return inFullPath.substring(0, 20) + "..."; + // string is too long + return inString.substring(0, 20) + "..."; } } diff --git a/tim/prune/gui/DisplayUtils.java b/tim/prune/gui/DisplayUtils.java index df706b7..49ca387 100644 --- a/tim/prune/gui/DisplayUtils.java +++ b/tim/prune/gui/DisplayUtils.java @@ -1,5 +1,7 @@ package tim.prune.gui; +import java.text.NumberFormat; + import tim.prune.I18nManager; /** @@ -7,6 +9,19 @@ import tim.prune.I18nManager; */ public abstract class DisplayUtils { + /** Number formatter for one decimal place */ + private static final NumberFormat FORMAT_ONE_DP = NumberFormat.getNumberInstance(); + + /** Static block to initialise the one d.p. formatter */ + static + { + FORMAT_ONE_DP.setMaximumFractionDigits(1); + FORMAT_ONE_DP.setMinimumFractionDigits(1); + } + /** Flexible number formatter with different decimal places */ + private static final NumberFormat FORMAT_FLEX = NumberFormat.getNumberInstance(); + + /** * Build a String to describe a time duration * @param inNumSecs number of seconds @@ -25,4 +40,34 @@ public abstract class DisplayUtils if (inNumSecs < 86400000L) return "" + (inNumSecs / 86400L) + I18nManager.getText("display.range.time.days"); return "big"; } + + /** + * @param inNumber number to format + * @return formatted number to one decimal place + */ + public static String formatOneDp(double inNumber) + { + return FORMAT_ONE_DP.format(inNumber); + } + + /** + * Format a number to a sensible precision + * @param inVal value to format + * @return formatted String using local format + */ + public static String roundedNumber(double inVal) + { + // Set precision of formatter + int numDigits = 0; + if (inVal < 1.0) + numDigits = 3; + else if (inVal < 10.0) + numDigits = 2; + else if (inVal < 100.0) + numDigits = 1; + // set formatter + FORMAT_FLEX.setMaximumFractionDigits(numDigits); + FORMAT_FLEX.setMinimumFractionDigits(numDigits); + return FORMAT_FLEX.format(inVal); + } } diff --git a/tim/prune/gui/GuiGridLayout.java b/tim/prune/gui/GuiGridLayout.java index 7b226b5..703f7dd 100644 --- a/tim/prune/gui/GuiGridLayout.java +++ b/tim/prune/gui/GuiGridLayout.java @@ -8,15 +8,17 @@ import javax.swing.JComponent; import javax.swing.JPanel; /** - * Class to make it easier to use GridBagLayout - * for a two-column, non-equal-width layout + * Class to make it easier to use GridBagLayout for a non-equal-width layout + * Default is two columns but can handle more */ public class GuiGridLayout { private GridBagLayout _layout = null; private GridBagConstraints _constraints = null; private JPanel _panel = null; - private boolean _allLeft = false; + private int _numColumns = 0; + private double[] _colWeights = null; + private boolean[] _rightAligns = null; private int _x = 0; private int _y = 0; @@ -26,20 +28,30 @@ public class GuiGridLayout */ public GuiGridLayout(JPanel inPanel) { - this(inPanel, false); + // Default is two columns, with more weight to the right-hand one; first column is right-aligned + this(inPanel, null, null); } /** * Constructor * @param inPanel panel using layout - * @param inAllLeft true to align all elements to left + * @param inColumnWeights array of column weights + * @param inAlignRights array of booleans, true for right alignment, false for left */ - public GuiGridLayout(JPanel inPanel, boolean inAllLeft) + public GuiGridLayout(JPanel inPanel, double[] inColumnWeights, boolean[] inAlignRights) { _panel = inPanel; - _allLeft = inAllLeft; _layout = new GridBagLayout(); _constraints = new GridBagConstraints(); + _colWeights = inColumnWeights; + _rightAligns = inAlignRights; + if (_colWeights == null || _rightAligns == null || _colWeights.length != _rightAligns.length + || _colWeights.length < 2) + { + _colWeights = new double[] {0.5, 1.0}; + _rightAligns = new boolean[] {true, false}; + } + _numColumns = _colWeights.length; _constraints.weightx = 1.0; _constraints.weighty = 0.0; _constraints.ipadx = 10; @@ -57,17 +69,25 @@ public class GuiGridLayout { _constraints.gridx = _x; _constraints.gridy = _y; - _constraints.weightx = (_x==0?0.5:1.0); + _constraints.weightx = _colWeights[_x]; // set anchor - _constraints.anchor = ((_x == 0 && !_allLeft)?GridBagConstraints.LINE_END:GridBagConstraints.LINE_START); + _constraints.anchor = (_rightAligns[_x]?GridBagConstraints.LINE_END:GridBagConstraints.LINE_START); _layout.setConstraints(inComponent, _constraints); // add to panel _panel.add(inComponent); // work out next position _x++; - if (_x > 1) { - _x = 0; - _y++; + if (_x >= _numColumns) { + nextRow(); } } + + /** + * Go to the next row of the grid + */ + public void nextRow() + { + _x = 0; + _y++; + } } diff --git a/tim/prune/gui/IconManager.java b/tim/prune/gui/IconManager.java index 72537b4..799e90f 100644 --- a/tim/prune/gui/IconManager.java +++ b/tim/prune/gui/IconManager.java @@ -71,6 +71,13 @@ public abstract class IconManager /** Icon for stopping the current audio clip */ public static final String STOP_AUDIO = "stop_audio.gif"; + /** Icon for a given entry being valid (green tick) */ + public static final String ENTRY_VALID = "entry_valid.gif"; + /** Icon for a given entry being invalid (red cross) */ + public static final String ENTRY_INVALID = "entry_invalid.gif"; + /** Icon for a given entry being empty (blank) */ + public static final String ENTRY_NONE = "entry_none.gif"; + /** * Get the specified image * @param inFilename filename of image (using constants) diff --git a/tim/prune/gui/MenuManager.java b/tim/prune/gui/MenuManager.java index c5501ee..16a0a66 100644 --- a/tim/prune/gui/MenuManager.java +++ b/tim/prune/gui/MenuManager.java @@ -21,6 +21,7 @@ import tim.prune.I18nManager; import tim.prune.UpdateMessageBroker; import tim.prune.config.Config; import tim.prune.data.AudioClip; +import tim.prune.data.Field; import tim.prune.data.Photo; import tim.prune.data.RecentFile; import tim.prune.data.RecentFileList; @@ -47,6 +48,7 @@ public class MenuManager implements DataSubscriber private JMenuItem _exportGpxItem = null; private JMenuItem _exportPovItem = null; private JMenuItem _exportSvgItem = null; + private JMenuItem _exportImageItem = null; private JMenu _recentFileMenu = null; private JMenuItem _undoItem = null; private JMenuItem _clearUndoItem = null; @@ -85,6 +87,8 @@ public class MenuManager implements DataSubscriber private JMenuItem _downloadOsmItem = null; private JMenuItem _distanceItem = null; private JMenuItem _fullRangeDetailsItem = null; + private JMenuItem _estimateTimeItem = null; + private JMenuItem _learnEstimationParams = null; private JMenuItem _saveExifItem = null; private JMenuItem _photoPopupItem = null; private JMenuItem _selectNoPhotoItem = null; @@ -217,7 +221,11 @@ public class MenuManager implements DataSubscriber // Svg _exportSvgItem = makeMenuItem(FunctionLibrary.FUNCTION_SVGEXPORT, false); fileMenu.add(_exportSvgItem); + // Image + _exportImageItem = makeMenuItem(FunctionLibrary.FUNCTION_IMAGEEXPORT, false); + fileMenu.add(_exportImageItem); fileMenu.addSeparator(); + // Exit JMenuItem exitMenuItem = new JMenuItem(I18nManager.getText("menu.file.exit")); exitMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { @@ -312,6 +320,9 @@ public class MenuManager implements DataSubscriber trackMenu.add(searchWikipediaNamesItem); _downloadOsmItem = makeMenuItem(FunctionLibrary.FUNCTION_DOWNLOAD_OSM, false); trackMenu.add(_downloadOsmItem); + trackMenu.addSeparator(); + _learnEstimationParams = makeMenuItem(FunctionLibrary.FUNCTION_LEARN_ESTIMATION_PARAMS, false); + trackMenu.add(_learnEstimationParams); menubar.add(trackMenu); // Range menu @@ -450,7 +461,7 @@ public class MenuManager implements DataSubscriber public void actionPerformed(ActionEvent e) { Config.setConfigBoolean(Config.KEY_SHOW_MAP, _mapCheckbox.isSelected()); UpdateMessageBroker.informSubscribers(MAPSERVER_CHANGED); - } + } }); viewMenu.add(_mapCheckbox); // Turn off the sidebars @@ -508,12 +519,16 @@ public class MenuManager implements DataSubscriber // Charts _chartItem = makeMenuItem(FunctionLibrary.FUNCTION_CHARTS, false); viewMenu.add(_chartItem); + viewMenu.addSeparator(); // Distances _distanceItem = makeMenuItem(FunctionLibrary.FUNCTION_DISTANCES, false); viewMenu.add(_distanceItem); // full range details _fullRangeDetailsItem = makeMenuItem(FunctionLibrary.FUNCTION_FULL_RANGE_DETAILS, false); viewMenu.add(_fullRangeDetailsItem); + // estimate time + _estimateTimeItem = makeMenuItem(FunctionLibrary.FUNCTION_ESTIMATE_TIME, false); + viewMenu.add(_estimateTimeItem); menubar.add(viewMenu); // Add photo menu @@ -613,8 +628,6 @@ public class MenuManager implements DataSubscriber settingsMenu.add(_onlineCheckbox); settingsMenu.add(makeMenuItem(FunctionLibrary.FUNCTION_SET_DISK_CACHE)); settingsMenu.addSeparator(); - // Set kmz image size - settingsMenu.add(makeMenuItem(FunctionLibrary.FUNCTION_SET_KMZ_IMAGE_SIZE)); // Set program paths settingsMenu.add(makeMenuItem(FunctionLibrary.FUNCTION_SET_PATHS)); // Set colours @@ -825,22 +838,25 @@ public class MenuManager implements DataSubscriber */ public void dataUpdated(byte inUpdateType) { - boolean hasData = (_track != null && _track.getNumPoints() > 0); + final boolean hasData = _track != null && _track.getNumPoints() > 0; + final boolean hasMultiplePoints = hasData && _track.getNumPoints() > 1; + // set functions which require data _sendGpsItem.setEnabled(hasData); _saveItem.setEnabled(hasData); _saveButton.setEnabled(hasData); _exportKmlItem.setEnabled(hasData); _exportGpxItem.setEnabled(hasData); - _exportPovItem.setEnabled(hasData); - _exportSvgItem.setEnabled(hasData); + _exportPovItem.setEnabled(hasMultiplePoints); + _exportSvgItem.setEnabled(hasMultiplePoints); + _exportImageItem.setEnabled(hasMultiplePoints); _compressItem.setEnabled(hasData); _markRectangleItem.setEnabled(hasData); _deleteMarkedPointsItem.setEnabled(hasData && _track.hasMarkedPoints()); _rearrangeMenu.setEnabled(hasData && _track.hasTrackPoints() && _track.hasWaypoints()); _selectAllItem.setEnabled(hasData); _selectNoneItem.setEnabled(hasData); - _show3dItem.setEnabled(hasData); + _show3dItem.setEnabled(hasMultiplePoints); _chartItem.setEnabled(hasData); _browserMapMenu.setEnabled(hasData); _distanceItem.setEnabled(hasData); @@ -850,6 +866,7 @@ public class MenuManager implements DataSubscriber _lookupWikipediaItem.setEnabled(hasData); _downloadOsmItem.setEnabled(hasData); _findWaypointItem.setEnabled(hasData && _track.hasWaypoints()); + // is undo available? boolean hasUndo = !_app.getUndoStack().isEmpty(); _undoItem.setEnabled(hasUndo); @@ -882,7 +899,7 @@ public class MenuManager implements DataSubscriber _connectButton.setEnabled(connectAvailable); _disconnectPhotoItem.setEnabled(hasPhoto && currentPhoto.getDataPoint() != null); _correlatePhotosItem.setEnabled(anyPhotos && hasData); - _rearrangePhotosItem.setEnabled(anyPhotos && hasData && _track.getNumPoints() > 1); + _rearrangePhotosItem.setEnabled(anyPhotos && hasMultiplePoints); _removePhotoItem.setEnabled(hasPhoto); _rotatePhotoLeft.setEnabled(hasPhoto); _rotatePhotoRight.setEnabled(hasPhoto); @@ -909,6 +926,9 @@ public class MenuManager implements DataSubscriber _convertNamesToTimesItem.setEnabled(hasRange && _track.hasWaypoints()); _deleteFieldValuesItem.setEnabled(hasRange); _fullRangeDetailsItem.setEnabled(hasRange); + _estimateTimeItem.setEnabled(hasRange); + _learnEstimationParams.setEnabled(hasData && _track.hasTrackPoints() && _track.hasData(Field.TIMESTAMP) + && _track.hasAltitudeData()); // Is the currently selected point outside the current range? boolean canCutAndMove = hasRange && hasPoint && (_selection.getCurrentPointIndex() < _selection.getStart() diff --git a/tim/prune/gui/ProgressDialog.java b/tim/prune/gui/ProgressDialog.java new file mode 100644 index 0000000..d09087e --- /dev/null +++ b/tim/prune/gui/ProgressDialog.java @@ -0,0 +1,115 @@ +package tim.prune.gui; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; + +import tim.prune.I18nManager; + +/** + * Class to show a simple progress dialog + * similar to swing's ProgressMonitor but with a few + * modifications + */ +public class ProgressDialog +{ + /** Parent frame */ + private JFrame _parentFrame = null; + /** Key for title text */ + private String _titleKey = null; + /** function dialog */ + private JDialog _dialog = null; + /** Progress bar for function */ + private JProgressBar _progressBar = null; + /** Cancel flag */ + private boolean _cancelled = false; + + + /** + * Constructor + * @param inParentFrame parent frame + * @param inNameKey key for title + */ + public ProgressDialog(JFrame inParentFrame, String inNameKey) + { + _parentFrame = inParentFrame; + _titleKey = inNameKey; + } + + public void show() + { + if (_dialog == null) + { + _dialog = new JDialog(_parentFrame, I18nManager.getText(_titleKey), false); + _dialog.setLocationRelativeTo(_parentFrame); + _dialog.getContentPane().add(makeDialogComponents()); + _dialog.pack(); + } + _progressBar.setMinimum(0); + _progressBar.setMaximum(100); + _progressBar.setValue(0); + _progressBar.setIndeterminate(true); + _cancelled = false; + _dialog.setVisible(true); + } + + /** + * Make the dialog components + * @return the GUI components for the dialog + */ + private Component makeDialogComponents() + { + JPanel dialogPanel = new JPanel(); + dialogPanel.setLayout(new BorderLayout()); + dialogPanel.add(new JLabel(I18nManager.getText("confirm.running")), BorderLayout.NORTH); + _progressBar = new JProgressBar(); + _progressBar.setPreferredSize(new Dimension(250, 30)); + dialogPanel.add(_progressBar, BorderLayout.CENTER); + // Cancel button at the bottom + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + _cancelled = true; + _dialog.dispose(); + } + }); + buttonPanel.add(cancelButton); + dialogPanel.add(buttonPanel, BorderLayout.SOUTH); + return dialogPanel; + } + + /** Set the maximum value of the progress bar */ + public void setMaximum(int inMax) { + _progressBar.setMaximum(inMax); + _progressBar.setIndeterminate(inMax <= 1); + } + + /** Set the current value of the progress bar */ + public void setValue(int inValue) { + _progressBar.setValue(inValue); + } + + /** Close the dialog */ + public void dispose() { + _dialog.dispose(); + } + + /** + * @return true if cancel button was pressed + */ + public boolean isCancelled() { + return _cancelled; + } +} diff --git a/tim/prune/gui/StatusIcon.java b/tim/prune/gui/StatusIcon.java new file mode 100644 index 0000000..26adb1b --- /dev/null +++ b/tim/prune/gui/StatusIcon.java @@ -0,0 +1,68 @@ +package tim.prune.gui; + +import javax.swing.ImageIcon; +import javax.swing.JLabel; + +/** + * Little status icon for green tick (valid) or red cross (not valid) + */ +public class StatusIcon extends JLabel +{ + /** Current status */ + private Status _currStatus = Status.BLANK; + + private static ImageIcon _blankIcon = IconManager.getImageIcon(IconManager.ENTRY_NONE); + private static ImageIcon _validIcon = IconManager.getImageIcon(IconManager.ENTRY_VALID); + private static ImageIcon _invalidIcon = IconManager.getImageIcon(IconManager.ENTRY_INVALID); + + /** + * Three possible states for icon + */ + private enum Status { + BLANK, + VALID, + INVALID + } + + /** + * Constructor + */ + public StatusIcon() + { + super(_blankIcon); + _currStatus = Status.BLANK; + } + + /** + * Set the status to blank + */ + public void setStatusBlank() + { + if (_currStatus != Status.BLANK) { + setIcon(_blankIcon); + _currStatus = Status.BLANK; + } + } + + /** + * Set the status to valid + */ + public void setStatusValid() + { + if (_currStatus != Status.VALID) { + setIcon(_validIcon); + _currStatus = Status.VALID; + } + } + + /** + * Set the status to not valid + */ + public void setStatusInvalid() + { + if (_currStatus != Status.INVALID) { + setIcon(_invalidIcon); + _currStatus = Status.INVALID; + } + } +} diff --git a/tim/prune/gui/WholeNumberField.java b/tim/prune/gui/WholeNumberField.java index 09f676d..ba953fc 100644 --- a/tim/prune/gui/WholeNumberField.java +++ b/tim/prune/gui/WholeNumberField.java @@ -57,7 +57,7 @@ public class WholeNumberField extends JTextField */ public WholeNumberField(int inMaxDigits) { - super("0"); + super(inMaxDigits); setDocument(new WholeNumberDocument(inMaxDigits)); } diff --git a/tim/prune/gui/WizardLayout.java b/tim/prune/gui/WizardLayout.java new file mode 100644 index 0000000..1a5a8f1 --- /dev/null +++ b/tim/prune/gui/WizardLayout.java @@ -0,0 +1,102 @@ +package tim.prune.gui; + +import java.awt.CardLayout; +import java.awt.Component; +import javax.swing.JPanel; + +/** + * Layout class enhancing the regular card layout to add the ability to + * see which is the current card, how many cards there are, previous / next etc + */ +public class WizardLayout extends CardLayout +{ + private JPanel _panel = null; + private int _currentCard = 0; + private int _numCards = 0; + + /** + * Constructor + * @param inPanel panel controlled by this layout + */ + public WizardLayout(JPanel inPanel) + { + super(); + _panel = inPanel; + _panel.setLayout(this); + } + + /** + * Add a card to this layout + * @param inCard + */ + public void addCard(Component inCard) + { + _panel.add(inCard, "card" + _numCards); + _numCards++; + } + + /** + * @return current card index (from 0) + */ + public int getCurrentCardIndex() { + return _currentCard; + } + + /** + * Go to the first card + */ + public void showFirstCard() + { + first(_panel); + _currentCard = 0; + } + + /** + * Go to the next card + */ + public void showNextCard() + { + if (_currentCard < (_numCards-1)) + { + next(_panel); + _currentCard++; + } + } + + /** + * Go to the previous card + */ + public void showPreviousCard() + { + if (_currentCard > 0) + { + previous(_panel); + _currentCard--; + } + } + + /** + * @return true if this is the first card + */ + public boolean isFirstCard() { + return _currentCard == 0; + } + + /** + * @return true if this is the last card + */ + public boolean isLastCard() { + return _currentCard == (_numCards-1); + } + + /** + * @param inIndex index (from 0) of the card to show + */ + public void showCard(int inIndex) + { + if (inIndex >= 0 && inIndex < _numCards) { + show(_panel, "card" + inIndex); + _currentCard = inIndex; + } + } +} diff --git a/tim/prune/gui/images/add_photo_icon.png b/tim/prune/gui/images/add_photo_icon.png old mode 100755 new mode 100644 diff --git a/tim/prune/gui/images/add_textfile_icon.png b/tim/prune/gui/images/add_textfile_icon.png old mode 100755 new mode 100644 diff --git a/tim/prune/gui/images/entry_invalid.gif b/tim/prune/gui/images/entry_invalid.gif new file mode 100644 index 0000000000000000000000000000000000000000..8612eaff5994f2b70514365e717d6ce1597020c2 GIT binary patch literal 84 zcmZ?wbhEHbWM|-DSj52apMhb&y?wpC{r~#<{qgbl_wWA?28usf7#SGY8FUzc0Hl_I iSu$edYO{nHswagRBbYXr$%Mro6!z^}z~;)tU=0A^iW{c@ literal 0 HcmV?d00001 diff --git a/tim/prune/gui/images/entry_none.gif b/tim/prune/gui/images/entry_none.gif new file mode 100644 index 0000000000000000000000000000000000000000..720eeec96fd325ccc057c756b2d32fdebbc02e48 GIT binary patch literal 67 zcmZ?wbhEHbWM|-DIK;%j5O2qj7yJMJ|JwaEKoSlVf3h$#FfcRdFaQBaEdw)$#jZR5 L3{FjCV6X-NvS%8H literal 0 HcmV?d00001 diff --git a/tim/prune/gui/images/entry_valid.gif b/tim/prune/gui/images/entry_valid.gif new file mode 100644 index 0000000000000000000000000000000000000000..9ca8275e6263512bdba8a0ed8313042972a1a052 GIT binary patch literal 78 zcmZ?wbhEHbWM|-DSj57>5O2qj7yJMJ|JwaEK#~Cl6o0ZXGBB_(=zs)3Y8jY?J$7x< WI($&!mP}#{huijf$2z>27_0$JWEGhJ literal 0 HcmV?d00001 diff --git a/tim/prune/gui/map/MapCanvas.java b/tim/prune/gui/map/MapCanvas.java index 96ff223..a6d5a88 100644 --- a/tim/prune/gui/map/MapCanvas.java +++ b/tim/prune/gui/map/MapCanvas.java @@ -390,25 +390,7 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe int selectedPoint = _selection.getCurrentPointIndex(); if (selectedPoint >= 0 && _dragFromX == -1 && selectedPoint != _prevSelectedPoint) { - int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(selectedPoint)); - int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(selectedPoint)); - int panX = 0; - int panY = 0; - if (px < PAN_DISTANCE) { - panX = px - AUTOPAN_DISTANCE; - } - else if (px > (getWidth()-PAN_DISTANCE)) { - panX = AUTOPAN_DISTANCE + px - getWidth(); - } - if (py < PAN_DISTANCE) { - panY = py - AUTOPAN_DISTANCE; - } - if (py > (getHeight()-PAN_DISTANCE)) { - panY = AUTOPAN_DISTANCE + py - getHeight(); - } - if (panX != 0 || panY != 0) { - _mapPosition.pan(panX, panY); - } + autopanToPoint(selectedPoint); } _prevSelectedPoint = selectedPoint; } @@ -446,7 +428,7 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe inG.drawLine(_dragFromX, _dragToY, _dragToX, _dragToY); } break; - + case MODE_DRAW_POINTS_CONT: // draw line to mouse position to show drawing mode inG.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_POINT)); @@ -469,6 +451,45 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe paintChildren(inG); } + /** + * @return true if the currently selected point is visible, false if off-screen or nothing selected + */ + private boolean isCurrentPointVisible() + { + if (_trackInfo.getCurrentPoint() == null) {return false;} + final int selectedPoint = _selection.getCurrentPointIndex(); + final int xFromCentre = Math.abs(_mapPosition.getXFromCentre(_track.getX(selectedPoint))); + if (xFromCentre > (getWidth()/2)) {return false;} + final int yFromCentre = Math.abs(_mapPosition.getYFromCentre(_track.getY(selectedPoint))); + return yFromCentre < (getHeight()/2); + } + + /** + * If the specified point isn't visible, pan to it + * @param inIndex index of selected point + */ + private void autopanToPoint(int inIndex) + { + int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(inIndex)); + int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(inIndex)); + int panX = 0; + int panY = 0; + if (px < PAN_DISTANCE) { + panX = px - AUTOPAN_DISTANCE; + } + else if (px > (getWidth()-PAN_DISTANCE)) { + panX = AUTOPAN_DISTANCE + px - getWidth(); + } + if (py < PAN_DISTANCE) { + panY = py - AUTOPAN_DISTANCE; + } + if (py > (getHeight()-PAN_DISTANCE)) { + panY = AUTOPAN_DISTANCE + py - getHeight(); + } + if (panX != 0 || panY != 0) { + _mapPosition.pan(panX, panY); + } + } /** * Paint the map tiles and the points on to the _mapImage @@ -550,8 +571,8 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe { pointsPainted = paintPoints(g); } - catch (NullPointerException npe) { // ignore, probably due to data being changed during drawing - } + catch (NullPointerException npe) {} // ignore, probably due to data being changed during drawing + catch (ArrayIndexOutOfBoundsException obe) {} // also ignore // free g g.dispose(); @@ -658,6 +679,7 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe int nameHeight = fm.getHeight(); if (anyWaypoints) { + int numWaypoints = 0; for (int i=0; i<_track.getNumPoints(); i++) { if (_track.getPoint(i).isWaypoint()) @@ -668,11 +690,18 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe { inG.fillRect(px-3, py-3, 6, 6); pointsPainted++; + numWaypoints++; } } } + // Take more care with waypoint names if less than 100 are visible + final int numNameSteps = (numWaypoints > 100 ? 1 : 4); + final int numPointSteps = (numWaypoints > 1000 ? 2 : 1); + // Loop over points again, now draw names for waypoints - for (int i=0; i<_track.getNumPoints(); i++) + int[] nameXs = {0, 0, 0, 0}; + int[] nameYs = {0, 0, 0, 0}; + for (int i=0; i<_track.getNumPoints(); i += numPointSteps) { if (_track.getPoint(i).isWaypoint()) { @@ -685,19 +714,21 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe int nameWidth = fm.stringWidth(waypointName); boolean drawnName = false; // Make arrays for coordinates right left up down - int[] nameXs = {px + 2, px - nameWidth - 2, px - nameWidth/2, px - nameWidth/2}; - int[] nameYs = {py + (nameHeight/2), py + (nameHeight/2), py - 2, py + nameHeight + 2}; - for (int extraSpace = 4; extraSpace < 13 && !drawnName; extraSpace+=2) + nameXs[0] = px + 2; nameXs[1] = px - nameWidth - 2; + nameXs[2] = nameXs[3] = px - nameWidth/2; + nameYs[0] = nameYs[1] = py + (nameHeight/2); + nameYs[2] = py - 2; nameYs[3] = py + nameHeight + 2; + for (int extraSpace = 0; extraSpace < numNameSteps && !drawnName; extraSpace++) { // Shift arrays for coordinates right left up down - nameXs[0] += 2; nameXs[1] -= 2; - nameYs[2] -= 2; nameYs[3] += 2; + nameXs[0] += 3; nameXs[1] -= 3; + nameYs[2] -= 3; nameYs[3] += 3; // Check each direction in turn right left up down for (int a=0; a<4; a++) { if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < winWidth && nameYs[a] < winHeight && (nameYs[a] - nameHeight) > 0 - && !overlapsPoints(nameXs[a], nameYs[a], nameWidth, nameHeight, textColour)) + && !MapUtils.overlapsPoints(_mapImage, nameXs[a], nameYs[a], nameWidth, nameHeight, textColour)) { // Found a rectangle to fit - draw name here and quit inG.drawString(waypointName, nameXs[a], nameYs[a]); @@ -813,50 +844,6 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe } } - /** - * Tests whether there are any dark pixels within the specified x,y rectangle - * @param inX left X coordinate - * @param inY bottom Y coordinate - * @param inWidth width of rectangle - * @param inHeight height of rectangle - * @param inTextColour colour of text - * @return true if the rectangle overlaps stuff too close to the given colour - */ - private boolean overlapsPoints(int inX, int inY, int inWidth, int inHeight, Color inTextColour) - { - // each of the colour channels must be further away than this to count as empty - final int BRIGHTNESS_LIMIT = 80; - final int textRGB = inTextColour.getRGB(); - final int textLow = textRGB & 255; - final int textMid = (textRGB >> 8) & 255; - final int textHigh = (textRGB >> 16) & 255; - try - { - // loop over x coordinate of rectangle - for (int x=0; x> 8) & 255; - int pixHigh = (pixelColor >> 16) & 255; - //int fourthBit = (pixelColor >> 24) & 255; // alpha ignored - // If colours are too close in any channel then it's an overlap - if (Math.abs(pixLow-textLow) < BRIGHTNESS_LIMIT || - Math.abs(pixMid-textMid) < BRIGHTNESS_LIMIT || - Math.abs(pixHigh-textHigh) < BRIGHTNESS_LIMIT) {return true;} - } - } - } - catch (NullPointerException e) { - // ignore null pointers, just return false - } - return false; - } - /** * Make a semi-transparent colour for drawing with * @param inColour base colour (fully opaque) @@ -906,7 +893,12 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe */ public void zoomIn() { + // See if selected point is currently visible, if so (and autopan on) then autopan after zoom to keep it visible + boolean wasVisible = _autopanCheckBox.isSelected() && isCurrentPointVisible(); _mapPosition.zoomIn(); + if (wasVisible && !isCurrentPointVisible()) { + autopanToPoint(_selection.getCurrentPointIndex()); + } _recalculate = true; repaint(); } diff --git a/tim/prune/gui/map/MapUtils.java b/tim/prune/gui/map/MapUtils.java index 78206a3..49d9467 100644 --- a/tim/prune/gui/map/MapUtils.java +++ b/tim/prune/gui/map/MapUtils.java @@ -1,7 +1,10 @@ package tim.prune.gui.map; +import java.awt.Color; +import java.awt.image.BufferedImage; + /** - * Class to manage coordinate conversions for maps + * Class to manage coordinate conversions and other stuff for maps */ public abstract class MapUtils { @@ -49,4 +52,50 @@ public abstract class MapUtils double n = Math.PI * (1 - 2 * inY); return 180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); } + + /** + * Tests whether there are any dark pixels in the image within the specified x,y rectangle + * @param inImage image to test + * @param inX left X coordinate + * @param inY bottom Y coordinate + * @param inWidth width of rectangle + * @param inHeight height of rectangle + * @param inTextColour colour of text + * @return true if the rectangle overlaps stuff too close to the given colour + */ + public static boolean overlapsPoints(BufferedImage inImage, int inX, int inY, + int inWidth, int inHeight, Color inTextColour) + { + // each of the colour channels must be further away than this to count as empty + final int BRIGHTNESS_LIMIT = 80; + final int textRGB = inTextColour.getRGB(); + final int textLow = textRGB & 255; + final int textMid = (textRGB >> 8) & 255; + final int textHigh = (textRGB >> 16) & 255; + try + { + // loop over x coordinate of rectangle + for (int x=0; x> 8) & 255; + int pixHigh = (pixelColor >> 16) & 255; + //int fourthBit = (pixelColor >> 24) & 255; // alpha ignored + // If colours are too close in any channel then it's an overlap + if (Math.abs(pixLow-textLow) < BRIGHTNESS_LIMIT || + Math.abs(pixMid-textMid) < BRIGHTNESS_LIMIT || + Math.abs(pixHigh-textHigh) < BRIGHTNESS_LIMIT) {return true;} + } + } + } + catch (NullPointerException e) { + // ignore null pointers, just return false + } + return false; + } } diff --git a/tim/prune/gui/profile/AltitudeData.java b/tim/prune/gui/profile/AltitudeData.java index 9c19862..9600b16 100644 --- a/tim/prune/gui/profile/AltitudeData.java +++ b/tim/prune/gui/profile/AltitudeData.java @@ -30,9 +30,9 @@ public class AltitudeData extends ProfileData final double multFactor = _unitSet.getAltitudeUnit().getMultFactorFromStd(); if (_track != null) { - for (int i=0; i<_track.getNumPoints(); i++) + try { - try + for (int i=0; i<_track.getNumPoints(); i++) { DataPoint point = _track.getPoint(i); if (point != null && point.hasAltitude()) @@ -43,15 +43,16 @@ public class AltitudeData extends ProfileData if (value < _minValue || !_hasData) {_minValue = value;} if (value > _maxValue || !_hasData) {_maxValue = value;} - _hasData = true; + // if all values are zero then that's no data + _hasData = _hasData || (point.getAltitude().getValue() != 0); _pointHasData[i] = true; } else _pointHasData[i] = false; } - catch (ArrayIndexOutOfBoundsException obe) - {} // must be due to the track size changing during calculation - // assume that a redraw will be triggered } + catch (ArrayIndexOutOfBoundsException obe) + {} // must be due to the track size changing during calculation + // assume that a redraw will be triggered } } diff --git a/tim/prune/gui/profile/ArbitraryData.java b/tim/prune/gui/profile/ArbitraryData.java new file mode 100644 index 0000000..cf974a5 --- /dev/null +++ b/tim/prune/gui/profile/ArbitraryData.java @@ -0,0 +1,79 @@ +package tim.prune.gui.profile; + +import tim.prune.data.Field; +import tim.prune.data.Track; +import tim.prune.data.UnitSet; + +/** + * Class to provide a source of values for the profile chart + * using any arbitary (non-built-in) field, units unknown + */ +public class ArbitraryData extends ProfileData +{ + /** Field to use */ + private Field _field = null; + + /** + * Constructor + * @param inTrack track object + * @param inField field to use + */ + public ArbitraryData(Track inTrack, Field inField) + { + super(inTrack); + _field = inField; + } + + /** + * Get the data and populate the instance arrays + */ + public void init(UnitSet inUnitSet) + { + setUnitSet(inUnitSet); + initArrays(); + _hasData = false; + _minValue = _maxValue = 0.0; + if (_track != null) + { + for (int i=0; i<_track.getNumPoints(); i++) + { + // Get the value of the given field + boolean hasValue = false; + String value = _track.getPoint(i).getFieldValue(_field); + try + { + double dValue = Double.parseDouble(value); + _pointValues[i] = dValue; + if (dValue < _minValue || _minValue == 0.0) {_minValue = dValue;} + if (dValue > _maxValue) {_maxValue = dValue;} + hasValue = true; + _hasData = true; + } + catch (Exception e) {} // ignore nulls and non-numbers + _pointHasData[i] = hasValue; + } + } + } + + /** + * @return name of field + */ + public String getLabel() + { + return _field.getName(); + } + + /** + * @return the field object + */ + public Field getField() { + return _field; + } + + /** + * @return key for message when no values present + */ + public String getNoDataKey() { + return "display.novalues"; + } +} diff --git a/tim/prune/gui/profile/ProfileChart.java b/tim/prune/gui/profile/ProfileChart.java index 770a211..732669a 100644 --- a/tim/prune/gui/profile/ProfileChart.java +++ b/tim/prune/gui/profile/ProfileChart.java @@ -15,6 +15,8 @@ import javax.swing.JPopupMenu; import tim.prune.I18nManager; import tim.prune.config.ColourScheme; import tim.prune.config.Config; +import tim.prune.data.Field; +import tim.prune.data.FieldList; import tim.prune.data.TrackInfo; import tim.prune.gui.GenericDisplay; @@ -23,6 +25,17 @@ import tim.prune.gui.GenericDisplay; */ public class ProfileChart extends GenericDisplay implements MouseListener { + /** Inner class to handle popup menu clicks */ + class MenuClicker implements ActionListener + { + private Field _field = null; + MenuClicker(Field inField) {_field = inField;} + /** React to menu click by changing the field */ + public void actionPerformed(ActionEvent arg0) { + changeView(_field); + } + } + /** Current scale factor in x direction*/ private double _xScaleFactor = 0.0; /** Data to show on chart */ @@ -40,8 +53,6 @@ public class ProfileChart extends GenericDisplay implements MouseListener private static final Dimension MINIMUM_SIZE = new Dimension(200, 110); /** Colour to use for text if no data found */ private static final Color COLOR_NODATA_TEXT = Color.GRAY; - /** Chart type */ - private static enum ChartType {ALTITUDE, SPEED, VERT_SPEED}; /** @@ -255,23 +266,38 @@ public class ProfileChart extends GenericDisplay implements MouseListener altItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - changeView(ChartType.ALTITUDE); + changeView(Field.ALTITUDE); }}); _popup.add(altItem); JMenuItem speedItem = new JMenuItem(I18nManager.getText("fieldname.speed")); speedItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - changeView(ChartType.SPEED); + changeView(Field.SPEED); }}); _popup.add(speedItem); JMenuItem vertSpeedItem = new JMenuItem(I18nManager.getText("fieldname.verticalspeed")); vertSpeedItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - changeView(ChartType.VERT_SPEED); + changeView(Field.VERTICAL_SPEED); }}); _popup.add(vertSpeedItem); + // Go through track's master field list, see if any other fields to list + boolean addSeparator = true; + FieldList fields = _track.getFieldList(); + for (int i=0; i 0) { + makePopup(); + } repaint(); } @@ -346,19 +376,31 @@ public class ProfileChart extends GenericDisplay implements MouseListener /** * Called by clicking on popup menu to change the view - * @param inType selected chart type + * @param inField field to show */ - private void changeView(ChartType inType) + private void changeView(Field inField) { - if (inType == ChartType.ALTITUDE && !(_data instanceof AltitudeData)) + if (inField == Field.ALTITUDE) { - _data = new AltitudeData(_track); + if (!(_data instanceof AltitudeData)) { + _data = new AltitudeData(_track); + } } - else if (inType == ChartType.SPEED && !(_data instanceof SpeedData)) { - _data = new SpeedData(_track); + else if (inField == Field.SPEED) { + if (!(_data instanceof SpeedData)) { + _data = new SpeedData(_track); + } } - else if (inType == ChartType.VERT_SPEED && !(_data instanceof VerticalSpeedData)) { - _data = new VerticalSpeedData(_track); + else if (inField == Field.VERTICAL_SPEED) { + if (!(_data instanceof VerticalSpeedData)) { + _data = new VerticalSpeedData(_track); + } + } + else + { + if (!(_data instanceof ArbitraryData) || ((ArbitraryData)_data).getField() != inField) { + _data = new ArbitraryData(_track, inField); + } } _data.init(Config.getUnitSet()); repaint(); diff --git a/tim/prune/lang/prune-texts_af.properties b/tim/prune/lang/prune-texts_af.properties index 2d0e54a..09d72de 100644 --- a/tim/prune/lang/prune-texts_af.properties +++ b/tim/prune/lang/prune-texts_af.properties @@ -95,7 +95,6 @@ function.show3d=3D Vertoon function.distances=Afstande function.fullrangedetails=Vol reeks besonderhede function.setmapbg=Stel Kaart agtergrond -function.setkmzimagesize=Stel KMZ beeld groote function.setpaths=Stel program paaie function.getgpsies=Kry GPS spore function.lookupsrtm=Kry hoogtes vanaf SRTM @@ -188,7 +187,7 @@ dialog.exportpov.cameraz=Kamera Z dialog.exportpov.modelstyle=Model styl dialog.exportpov.ballsandsticks=Balle en stokkies dialog.exportpov.tubesandwalls=Buise en mure -dialog.exportpov.warningtracksize=Hierdie spoor het 'n groot aantal punte, wat Java3D miskien nie kan vertoon.\nIs jy seker jy wil voortgaan? +dialog.3d.warningtracksize=Hierdie spoor het 'n groot aantal punte, wat Java3D miskien nie kan vertoon.\nIs jy seker jy wil voortgaan? dialog.exportsvg.text=Selekteer die parameters vir die SVG uitvoer dialog.exportsvg.phi=Azimuth hoek \u03d5 dialog.exportsvg.theta=Opstandings angle \u03b8 diff --git a/tim/prune/lang/prune-texts_cz.properties b/tim/prune/lang/prune-texts_cz.properties index 6c5a9db..b922f3b 100644 --- a/tim/prune/lang/prune-texts_cz.properties +++ b/tim/prune/lang/prune-texts_cz.properties @@ -7,7 +7,7 @@ menu.file.addphotos=P\u0159idat fotografie menu.file.recentfiles=Naposledy otev\u0159en\u00e9 menu.file.save=Ulo\u017eit jako text menu.file.exit=Konec -menu.track=Trasa +menu.track=Stopa menu.track.undo=Undo menu.track.clearundo=Vypr\u00e1zdnit pam\u011b\u0165 undo menu.track.markrectangle=Ozna\u010dit body v obd\u00e9ln\u00edku @@ -15,7 +15,7 @@ menu.track.deletemarked=Smazat ozna\u010den\u00e9 body menu.track.rearrange=P\u0159euspo\u0159\u00e1dat z\u00e1jmov\u00e9 body menu.track.rearrange.start=V\u0161e na po\u010d\u00e1tek menu.track.rearrange.end=V\u0161e na konec -menu.track.rearrange.nearest=Zarovnat body na trasu +menu.track.rearrange.nearest=Zarovnat body na stopu menu.range=Rozmez\u00ed menu.range.all=Vybrat v\u0161e menu.range.none=Zru\u0161it v\u00fdb\u011br @@ -23,7 +23,7 @@ menu.range.start=Nastavit za\u010d\u00e1tek rozmez\u00ed menu.range.end=Nastavit konec rozmez\u00ed menu.range.average=St\u0159ed z v\u00fdb\u011bru menu.range.reverse=Obr\u00e1tit rozmez\u00ed -menu.range.mergetracksegments=Slou\u010dit \u010d\u00e1sti trasy +menu.range.mergetracksegments=Slou\u010dit \u010d\u00e1sti stopy menu.range.cutandmove=P\u0159en\u00e9st v\u00fdb\u011br menu.point=Bod menu.point.editpoint=Upravit bod @@ -49,7 +49,7 @@ menu.map.zoomout=Odd\u00e1lit menu.map.zoomfull=\u00dapln\u011b odd\u00e1lit menu.map.newpoint=Vytvo\u0159it nov\u00fd bod menu.map.drawpoints=Vytvo\u0159it n\u011bkolik bod\u016f -menu.map.connect=Propojit body trasy +menu.map.connect=Propojit body stopy menu.map.autopan=Automatika zorn\u00e9ho pole menu.map.showmap=Zobrazit mapu menu.map.showscalebar=Zobrazit m\u011b\u0159\u00edtko @@ -84,10 +84,11 @@ function.exportkml=Export KML function.exportgpx=Export GPX function.exportpov=Export POV function.exportsvg=Export SVG +function.exportimage=Export obrazu mapy function.editwaypointname=Nastavit n\u00e1zev v\u00fdzna\u010dn\u00e9ho bodu -function.compress=Komprimovat trasu +function.compress=Komprimovat stopu function.deleterange=Smazat rozmez\u00ed -function.croptrack=O\u0159\u00edznout trasu +function.croptrack=O\u0159\u00edznout stopu function.interpolate=Interpolovat body function.addtimeoffset=P\u0159idat \u010dasov\u00fd posun function.addaltitudeoffset=P\u0159idat v\u00fd\u0161kov\u00fd posun @@ -99,11 +100,12 @@ function.charts=Grafy function.show3d=Trojrozm\u011brn\u011b function.distances=Vzd\u00e1lenosti function.fullrangedetails=Detaily rozmez\u00ed +function.estimatetime=Odhad \u010dasu +function.learnestimationparams=Anal\u00fdza stopy pro odhad \u010dasu function.setmapbg=Nastavit pozad\u00ed -function.setkmzimagesize=Nastavit velikost exportu KMZ function.setpaths=Nastavit cestu k program\u016fm -function.getgpsies=St\u00e1hnout trasy z Gpsies -function.uploadgpsies=Nahr\u00e1t trasu na Gpsies +function.getgpsies=St\u00e1hnout stopy z Gpsies +function.uploadgpsies=Nahr\u00e1t stopu na Gpsies function.lookupsrtm=Na\u010d\u00edst nadm. v\u00fd\u0161ku ze SRTM function.getwikipedia=Hledat na Wikipedii podle vzd\u00e1lenosti function.searchwikipedianames=Hledat na Wikipedii podle jm\u00e9na @@ -159,8 +161,12 @@ dialog.openoptions.deliminfo.records=z\u00e1znam\u016f, s dialog.openoptions.deliminfo.fields=poli dialog.openoptions.deliminfo.norecords=\u017d\u00e1dn\u00e9 z\u00e1znamy dialog.openoptions.altitudeunits=Jednotky v\u00fd\u0161ky -dialog.open.contentsdoubled=Tento soubor obsahuje dv\u011b kopie ka\u017ed\u00e9ho bodu,\nv\u017edy jednou jako body trasy a jednou jako v\u00fdzna\u010dn\u00e9 body. -dialog.selecttracks.intro=Vyberte trasu nebo trasy k na\u010dten\u00ed +dialog.openoptions.speedunits=Jednotky rychlosti +dialog.openoptions.vertspeedunits=Jednotky vertik\u00e1ln\u00ed rychlosti +dialog.openoptions.vspeed.positiveup=Kladn\u00e1 rychlost znamen\u00e1 stoup\u00e1n\u00ed +dialog.openoptions.vspeed.positivedown=Kladn\u00e1 rychlost znamen\u00e1 kles\u00e1n\u00ed +dialog.open.contentsdoubled=Tento soubor obsahuje dv\u011b kopie ka\u017ed\u00e9ho bodu,\nv\u017edy jednou jako body stopy a jednou jako v\u00fdzna\u010dn\u00e9 body. +dialog.selecttracks.intro=Vyberte stopu nebo stopy k na\u010dten\u00ed dialog.selecttracks.noname=Bez n\u00e1zvu dialog.jpegload.subdirectories=V\u010detn\u011b podadres\u00e1\u0159\u016f dialog.jpegload.loadjpegswithoutcoords=V\u010detn\u011b fotografi\u00ed bez sou\u0159adnic @@ -171,11 +177,35 @@ dialog.gpsload.nogpsbabel=Nenalezen program gpsbabel. Pokra\u010dovat? dialog.gpsload.device=Ozna\u010den\u00ed za\u0159\u00edzen\u00ed dialog.gpsload.format=Form\u00e1t dialog.gpsload.getwaypoints=Na\u010d\u00edst v\u00fdzna\u010dn\u00e9 body -dialog.gpsload.gettracks=Na\u010d\u00edst trasy +dialog.gpsload.gettracks=Na\u010d\u00edst stopy dialog.gpsload.save=Ulo\u017eit do souboru dialog.gpssend.sendwaypoints=Poslat bod -dialog.gpssend.sendtracks=Poslat trasy -dialog.gpssend.trackname=N\u00e1zev trasy +dialog.gpssend.sendtracks=Poslat stopy +dialog.gpssend.trackname=N\u00e1zev stopy +dialog.gpsbabel.filters=Filtry +dialog.addfilter.title=P\u0159idat filtr +dialog.gpsbabel.filter.discard=Vynech\u00e1n\u00ed +dialog.gpsbabel.filter.simplify=Zjednodu\u0161en\u00ed +dialog.gpsbabel.filter.distance=Vzd\u00e1lenost +dialog.gpsbabel.filter.interpolate=Interpolace +dialog.gpsbabel.filter.discard.intro=Body vynechat, kdy\u017e +dialog.gpsbabel.filter.discard.hdop=hdop > +dialog.gpsbabel.filter.discard.vdop=vdop > +dialog.gpsbabel.filter.discard.numsats=po\u010det satelit\u016f < +dialog.gpsbabel.filter.discard.nofix=bod nem\u00e1 fix +dialog.gpsbabel.filter.discard.unknownfix=fix bodu je nezn\u00e1m\u00fd +dialog.gpsbabel.filter.simplify.intro=Odstra\u0148ovat body dokud +dialog.gpsbabel.filter.simplify.maxpoints=po\u010det bod\u016f < +dialog.gpsbabel.filter.simplify.maxerror=nebo max. odchylka < +dialog.gpsbabel.filter.simplify.crosstrack=nap\u0159\u00ed\u010d stopami +dialog.gpsbabel.filter.simplify.length=rozd\u00edl d\u00e9lek +dialog.gpsbabel.filter.simplify.relative=relativn\u011b k hdop +dialog.gpsbabel.filter.distance.intro=Smazat body +dialog.gpsbabel.filter.distance.distance=pokud vzd\u00e1lenost < +dialog.gpsbabel.filter.distance.time=a \u010dasov\u00fd rozd\u00edl < +dialog.gpsbabel.filter.interpolate.intro=P\u0159idat dal\u0161\u00ed body mezi +dialog.gpsbabel.filter.interpolate.distance=pokud vzd\u00e1lenost > +dialog.gpsbabel.filter.interpolate.time=nebo \u010dasov\u00fd rozd\u00edl > dialog.saveoptions.title=Ulo\u017eit soubor dialog.save.fieldstosave=Ulo\u017eit pole dialog.save.table.field=Pole @@ -192,7 +222,10 @@ dialog.exportkml.text=Nadpis dat dialog.exportkml.altitude=V\u00fd\u0161ka nad hladinou mo\u0159e (pro letectv\u00ed) dialog.exportkml.kmz=Komprimovat do souboru kmz dialog.exportkml.exportimages=Vlo\u017eit n\u00e1hledy fotografi\u00ed -dialog.exportkml.trackcolour=Barva trasy +dialog.exportkml.imagesize=Velikost obr\u00e1zku +dialog.exportkml.trackcolour=Barva stopy +dialog.exportkml.standardkml=Standardn\u00ed KML +dialog.exportkml.extendedkml=Roz\u0161\u00ed\u0159en\u00e9 KML s \u010dasov\u00fdmi zna\u010dkami dialog.exportgpx.name=N\u00e1zev dialog.exportgpx.desc=Popis dialog.exportgpx.includetimestamps=Ulo\u017eit \u010dasov\u00e9 zna\u010dky @@ -208,23 +241,35 @@ dialog.exportpov.cameraz=Kamera Z dialog.exportpov.modelstyle=Model dialog.exportpov.ballsandsticks=Koule a ty\u010dky dialog.exportpov.tubesandwalls=Roury a st\u011bny -dialog.exportpov.warningtracksize=Tato trasa sest\u00e1v\u00e1 z mnoha bod\u016f, kter\u00e9 mo\u017en\u00e1 Java3D nebude um\u011bt zobrazit.\nOpravdu chcete pokra\u010dovat? +dialog.3d.warningtracksize=Tato stopa sest\u00e1v\u00e1 z mnoha bod\u016f, kter\u00e9 mo\u017en\u00e1 Java3D nebude um\u011bt zobrazit.\nOpravdu chcete pokra\u010dovat? +dialog.exportpov.baseimage=Obr\u00e1zek jako podklad +dialog.exportpov.cannotmakebaseimage=Nelze zapsat podklad +dialog.baseimage.title=Podklad +dialog.baseimage.useimage=Pou\u017e\u00edt obr\u00e1zek +dialog.baseimage.mapsource=Zdroj mapy +dialog.baseimage.zoom=Zv\u011bt\u0161en\u00ed +dialog.baseimage.incomplete=Obr\u00e1zek ne\u00fapln\u00fd +dialog.baseimage.tiles=Dla\u017edic +dialog.baseimage.size=Velikost obr\u00e1zku dialog.exportsvg.text=Zvolte parametry exportu do SVG dialog.exportsvg.phi=Azimut \u03d5 dialog.exportsvg.theta=V\u00fd\u0161kov\u00fd \u00fahel \u03b8 dialog.exportsvg.gradients=Vypl\u0148ovat body barevn\u00fdm p\u0159echodem +dialog.exportimage.noimagepossible=Aby bylo mo\u017en\u00e9 mapu ulo\u017eit jako obr\u00e1zek, je t\u0159eba st\u00e1hnout a ulo\u017eit dla\u017edice +dialog.exportimage.drawtrack=Nakreslit stopu na mapu +dialog.exportimage.textscalepercent=Zv\u011bt\u0161en\u00ed fontu (%) dialog.pointtype.desc=Ulo\u017eit body n\u00e1sleduj\u00edc\u00edch typ\u016f: -dialog.pointtype.track=Body trasy +dialog.pointtype.track=Body stopy dialog.pointtype.waypoint=V\u00fdzna\u010dn\u00e9 body dialog.pointtype.photo=M\u00edsta s fotografiemi dialog.pointtype.audio=M\u00edsta s audionahr\u00e1vkami dialog.pointtype.selection=Jen v\u00fdb\u011br dialog.confirmreversetrack.title=Potvr\u010fte obr\u00e1cen\u00ed -dialog.confirmreversetrack.text=Tato trasa obsahuje \u010dasov\u00e9 zna\u010dky, jejich\u017e po\u0159ad\u00ed se obr\u00e1cen\u00edm zm\u011bn\u00ed.\nOpravdu chcete obr\u00e1tit v\u00fdb\u011br? +dialog.confirmreversetrack.text=Tato stopa obsahuje \u010dasov\u00e9 zna\u010dky, jejich\u017e po\u0159ad\u00ed se obr\u00e1cen\u00edm zm\u011bn\u00ed.\nOpravdu chcete obr\u00e1tit v\u00fdb\u011br? dialog.confirmcutandmove.title=Potvr\u010fte p\u0159esun -dialog.confirmcutandmove.text=Tato trasa obsahuje \u010dasov\u00e9 zna\u010dky, jejich\u017e po\u0159ad\u00ed se p\u0159esunem zm\u011bn\u00ed.\nOpravdu chcete v\u00fdb\u011br p\u0159esunout? +dialog.confirmcutandmove.text=Tato stopa obsahuje \u010dasov\u00e9 zna\u010dky, jejich\u017e po\u0159ad\u00ed se p\u0159esunem zm\u011bn\u00ed.\nOpravdu chcete v\u00fdb\u011br p\u0159esunout? dialog.interpolate.parameter.text=Po\u010det bod\u016f, kter\u00e9 se maj\u00ed vlo\u017eit mezi ka\u017ed\u00e9 dva po sob\u011b jdouc\u00ed body -dialog.interpolate.betweenwaypoints=Vlo\u017eit nov\u00e9 body trasy mezi v\u00fdzna\u010dn\u00fdmi body? +dialog.interpolate.betweenwaypoints=Vlo\u017eit nov\u00e9 body stopy mezi v\u00fdzna\u010dn\u00fdmi body? dialog.undo.title=Vr\u00e1tit akci (akce) dialog.undo.pretext=Pros\u00edm vyberte akci (akce) k vr\u00e1cen\u00ed dialog.undo.none.title=Nelze vr\u00e1tit @@ -233,7 +278,9 @@ dialog.clearundo.title=Vypr\u00e1zdnit pam\u011b\u0165 undo dialog.clearundo.text=Opravdu chcete vypr\u00e1zdnit pam\u011b\u0165 undo?\nNebude u\u017e mo\u017en\u00e9 akce vracet do p\u016fvodn\u00edho stavu! dialog.pointedit.title=Upravit bod dialog.pointedit.text=Vyberte pole k editaci a stiskn\u011bte 'Upravit' +dialog.pointedit.intro=Vyberte pole k zobrazen\u00ed a editaci hodnoty dialog.pointedit.table.field=Pole +dialog.pointedit.nofield=Nen\u00ed vybr\u00e1no \u017e\u00e1dn\u00e9 pole dialog.pointedit.table.value=Hodnota dialog.pointedit.table.changed=Zm\u011bn\u011bno dialog.pointedit.changevalue.text=Zadejte novou hodnotu pole @@ -269,7 +316,7 @@ dialog.charts.screen=V\u00fdstup na obrazovku dialog.charts.svg=V\u00fdstup do souboru SVG dialog.charts.svgwidth=\u0160\u00ed\u0159ka SVG dialog.charts.svgheight=V\u00fd\u0161ka SVG -dialog.charts.needaltitudeortimes=Trasa mus\u00ed obsahovat bu\u010f informaci o nadmo\u0159sk\u00e9 v\u00fd\u0161ce nebo o \u010dase, aby bylo mo\u017en\u00e9 vytv\u00e1\u0159et grafy +dialog.charts.needaltitudeortimes=Stopa mus\u00ed obsahovat bu\u010f informaci o nadmo\u0159sk\u00e9 v\u00fd\u0161ce nebo o \u010dase, aby bylo mo\u017en\u00e9 vytv\u00e1\u0159et grafy dialog.charts.gnuplotnotfound=Nepoda\u0159ilo se nal\u00e9zt gnuplot na dan\u00e9m um\u00edst\u011bn\u00ed dialog.distances.intro=P\u0159\u00edm\u00e9 vzd\u00e1lenosti mezi body dialog.distances.column.from=Z bodu @@ -279,6 +326,27 @@ dialog.distances.toofewpoints=Je t\u0159eba zadat v\u00edce bod\u016f, aby bylo dialog.fullrangedetails.intro=Zobrazuji detaily vybran\u00e9ho rozmez\u00ed dialog.fullrangedetails.coltotal=V\u010detn\u011b p\u0159eru\u0161en\u00ed dialog.fullrangedetails.colsegments=Bez p\u0159eru\u0161en\u00ed +dialog.estimatetime.details=Podrobnosti +dialog.estimatetime.gentle=M\u00edrn\u00e9 +dialog.estimatetime.steep=Prudk\u00e9 +dialog.estimatetime.climb=Stoup\u00e1n\u00ed +dialog.estimatetime.descent=Kles\u00e1n\u00ed +dialog.estimatetime.parameters=Parametry +dialog.estimatetime.parameters.timefor=\u010cas k +dialog.estimatetime.results=V\u00fdsledky +dialog.estimatetime.results.estimatedtime=Odhad \u010dasu +dialog.estimatetime.results.actualtime=Aktu\u00e1ln\u00ed \u010das +dialog.estimatetime.error.nodistance=Aby bylo mo\u017en\u00e9 odhadnout \u010das, je t\u0159eba vybrat body jedn\u00e9 stopy +dialog.estimatetime.error.noaltitudes=V\u00fdb\u011br neobsahuje \u00fadaje o nadmo\u0159sk\u00e9 v\u00fd\u0161ce +dialog.learnestimationparams.intro=Parametry vypo\u010d\u00edtan\u00e9 podle t\u00e9to stopy +dialog.learnestimationparams.averageerror=Pr\u016fm\u011brn\u00e1 chyba +dialog.learnestimationparams.combine=Tyto parametry je mo\u017en\u00e9 zkombinovat s aktu\u00e1ln\u00edmi hodnotami +dialog.learnestimationparams.combinedresults=V\u00fdsledn\u00e9 hodnoty +dialog.learnestimationparams.weight.100pccurrent=Ponechat st\u00e1vaj\u00edc\u00ed hodnoty +dialog.learnestimationparams.weight.current=st\u00e1vaj\u00edc\u00ed +dialog.learnestimationparams.weight.calculated=vypo\u010dten\u00e9 +dialog.learnestimationparams.weight.50pc=Pr\u016fm\u011br st\u00e1vaj\u00edc\u00edch a vypo\u010d\u00edtan\u00fdch hodnot +dialog.learnestimationparams.weight.100pccalculated=Pou\u017e\u00edt vypo\u010d\u00edtan\u00e9 hodnoty dialog.setmapbg.intro=Vyberte jeden ze zdroj\u016f map nebo p\u0159idejte nov\u00fd dialog.addmapsource.title=P\u0159idat nov\u00fd zdroj map dialog.addmapsource.sourcename=N\u00e1zev zdroje @@ -287,15 +355,15 @@ dialog.addmapsource.layer2url=Voliteln\u011b URL druh\u00e9 vrstvy dialog.addmapsource.maxzoom=Maxim\u00e1ln\u00ed zv\u011bt\u0161en\u00ed dialog.addmapsource.cloudstyle=\u010c\u00edslo stylu dialog.addmapsource.noname=Bez n\u00e1zvu -dialog.gpsies.column.name=N\u00e1zev trasy +dialog.gpsies.column.name=N\u00e1zev stopy dialog.gpsies.column.length=D\u00e9lka dialog.gpsies.description=Popis dialog.gpsies.nodescription=Bez popisu -dialog.gpsies.nonefound=Nenalezeny \u017e\u00e1dn\u00e9 trasy +dialog.gpsies.nonefound=Nenalezeny \u017e\u00e1dn\u00e9 stopy dialog.gpsies.username=U\u017eiv. jm\u00e9no k gpsies dialog.gpsies.password=Heslo k gpsies -dialog.gpsies.keepprivate=Trasu nezve\u0159ej\u0148ovat -dialog.gpsies.confirmopenpage=Otev\u0159\u00edt nahranou trasu v internetov\u00e9m prohl\u00ed\u017ee\u010di? +dialog.gpsies.keepprivate=Stopu nezve\u0159ej\u0148ovat +dialog.gpsies.confirmopenpage=Otev\u0159\u00edt nahranou stopu v internetov\u00e9m prohl\u00ed\u017ee\u010di? dialog.gpsies.activities=Aktivita dialog.gpsies.activity.trekking=Turistika dialog.gpsies.activity.walking=Ch\u016fze @@ -331,7 +399,7 @@ dialog.correlate.options.timelimit=\u010casov\u00fd limit dialog.correlate.options.nodistancelimit=Bez d\u00e9lkov\u00e9ho limitu dialog.correlate.options.distancelimit=D\u00e9lkov\u00fd limit dialog.correlate.options.correlate=Sladit -dialog.correlate.alloutsiderange=V\u0161echny polo\u017eky le\u017e\u00ed mimo \u010dasov\u00e9 rozmez\u00ed trasy, tak\u017ee nemohou b\u00fdt slad\u011bny.\nPokuste se zm\u011bnit \u010dasov\u00fd posun nebo ru\u010dn\u011b sla\u010fte aspo\u0148 jednu polo\u017eku. +dialog.correlate.alloutsiderange=V\u0161echny polo\u017eky le\u017e\u00ed mimo \u010dasov\u00e9 rozmez\u00ed stopy, tak\u017ee nemohou b\u00fdt slad\u011bny.\nPokuste se zm\u011bnit \u010dasov\u00fd posun nebo ru\u010dn\u011b sla\u010fte aspo\u0148 jednu polo\u017eku. dialog.correlate.filetimes=\u010cas z\u00e1znamu souboru znamen\u00e1: dialog.correlate.filetimes2=audionahr\u00e1vky dialog.correlate.correltimes=Sladit tento okam\u017eik nahr\u00e1vky: @@ -358,9 +426,9 @@ dialog.compress.douglaspeucker.title=Douglasova-Peuckerova komprese dialog.compress.douglaspeucker.paramdesc=Povolen\u00e1 odchylka dialog.compress.summarylabel=Bod\u016f ke smaz\u00e1n\u00ed dialog.compress.confirm1=Bylo ozna\u010deno celkem -dialog.compress.confirm2=bod\u016f.\nBody je mo\u017en\u00e9 smazat volbou Trasa->Smazat ozna\u010den\u00e9 body +dialog.compress.confirm2=bod\u016f.\nBody je mo\u017en\u00e9 smazat volbou Stopa->Smazat ozna\u010den\u00e9 body dialog.compress.confirmnone=Nebyly vybr\u00e1ny \u017e\u00e1dn\u00e9 body. -dialog.deletemarked.nonefound=Nemohou b\u00fdt odstran\u011bny \u017e\u00e1dn\u00e9 body trasy +dialog.deletemarked.nonefound=Nemohou b\u00fdt odstran\u011bny \u017e\u00e1dn\u00e9 body stopy dialog.pastecoordinates.desc=Zadejte sou\u0159adnice dialog.pastecoordinates.coords=Sou\u0159adnice dialog.pastecoordinates.nothingfound=Pros\u00edm ov\u011b\u0159te sou\u0159adnice a zkuste znovu @@ -405,11 +473,11 @@ dialog.checkversion.releasedate1=Tato verze byla vyd\u00e1na dialog.checkversion.releasedate2=. dialog.checkversion.download=Novou verzi m\u016f\u017eete st\u00e1hnout na adrese http://activityworkshop.net/software/gpsprune/download.html. dialog.keys.intro=M\u00edsto my\u0161i m\u016f\u017eete pou\u017e\u00edvat n\u00e1sleduj\u00edc\u00ed kl\u00e1vesov\u00e9 zkratky -dialog.keys.keylist=
\u0160ipkyPosunout mapu vlevo, vpravo, nahoru, dol\u016f
Ctrl + \u0161ipka vlevo, vpravoVybrat p\u0159edchoz\u00ed nebo n\u00e1sleduj\u00edc\u00ed bod
Ctrl + \u0161ipka nahoru, dol\u016fP\u0159ibl\u00ed\u017eit, odd\u00e1lit
Ctrl + PgUp, PgDownVybrat p\u0159edchoz\u00ed, n\u00e1sleduj\u00edc\u00ed \u010d\u00e1st trasy
Ctrl + Home, EndVybrat prvn\u00ed, posledn\u00ed bod
DelSmazat aktu\u00e1ln\u00ed bod
+dialog.keys.keylist=
\u0160ipkyPosunout mapu vlevo, vpravo, nahoru, dol\u016f
Ctrl + \u0161ipka vlevo, vpravoVybrat p\u0159edchoz\u00ed nebo n\u00e1sleduj\u00edc\u00ed bod
Ctrl + \u0161ipka nahoru, dol\u016fP\u0159ibl\u00ed\u017eit, odd\u00e1lit
Ctrl + PgUp, PgDownVybrat p\u0159edchoz\u00ed, n\u00e1sleduj\u00edc\u00ed \u010d\u00e1st stopy
Ctrl + Home, EndVybrat prvn\u00ed, posledn\u00ed bod
DelSmazat aktu\u00e1ln\u00ed bod
dialog.keys.normalmodifier=Ctrl dialog.keys.macmodifier=Command dialog.saveconfig.desc=N\u00e1sleduj\u00edc\u00ed volby mohou b\u00fdt ulo\u017eeny do konfigura\u010dn\u00edho souboru : -dialog.saveconfig.prune.trackdirectory=Adres\u00e1\u0159 s trasami +dialog.saveconfig.prune.trackdirectory=Adres\u00e1\u0159 s stopami dialog.saveconfig.prune.photodirectory=Ad\u0159es\u00e1\u0159 s fotografiemi dialog.saveconfig.prune.languagecode=K\u00f3d jazyka dialog.saveconfig.prune.languagefile=Soubor jazyka @@ -426,7 +494,7 @@ dialog.saveconfig.prune.kmzimagewidth=\u0160\u00ed\u0159ka bitmapy KMZ dialog.saveconfig.prune.kmzimageheight=V\u00fd\u0161ka bitmapy KMZ dialog.saveconfig.prune.colourscheme=Barevn\u00e9 sch\u00e9ma dialog.saveconfig.prune.linewidth=Tlou\u0161\u0165ka \u010d\u00e1ry -dialog.saveconfig.prune.kmltrackcolour=Barva trasy v KML +dialog.saveconfig.prune.kmltrackcolour=Barva stopy v KML dialog.saveconfig.prune.autosavesettings=Mo\u017enosti ukl\u00e1d\u00e1n\u00ed dialog.setpaths.intro=Je-li to t\u0159eba, m\u016f\u017eete nastavit cesty k extern\u00edm aplikac\u00edm: dialog.setpaths.found=Cesta nalezena? @@ -471,16 +539,13 @@ dialog.diskcache.deleted1=Smaz\u00e1no dialog.diskcache.deleted2=soubor\u016f z cache dialog.deletefieldvalues.intro=Vyberte pole, kter\u00e9 se m\u00e1 z aktu\u00e1ln\u00edho rozmez\u00ed odstranit dialog.deletefieldvalues.nofields=V tomto rozmez\u00ed nelze smazat \u017e\u00e1dn\u00e9 pole -dialog.setlinewidth.text=Zvolte tlou\u0161\u0165ku \u010d\u00e1ry, kterou se nakresl\u00ed trasa (1-4) +dialog.setlinewidth.text=Zvolte tlou\u0161\u0165ku \u010d\u00e1ry, kterou se nakresl\u00ed stopa (1-4) dialog.downloadosm.desc=Potvr\u010fte, \u017ee se maj\u00ed k dan\u00e9 oblasti st\u00e1hnout data OSM: dialog.searchwikipedianames.search=Vyhledat: # 3d window dialog.3d.title=Trojrozm\u011brn\u00e9 zobrazen\u00ed GpsPrune dialog.3d.altitudefactor=Faktor zd\u016frazn\u011bn\u00ed v\u00fd\u0161ky -dialog.3dlines.title=Linky m\u0159\u00ed\u017eky GpsPrune -dialog.3dlines.empty=Nejsou \u017e\u00e1dn\u00e9 linky k zobrazen\u00ed! -dialog.3dlines.intro=Toto jsou linky m\u0159\u00ed\u017eky trojrozm\u011brn\u00e9ho zobrazen\u00ed # Confirm messages confirm.loadfile=Data na\u010dtena ze souboru @@ -489,7 +554,7 @@ confirm.save.ok2=bod\u016f do souboru confirm.deletepoint.single=bod byl odstran\u011bn confirm.deletepoint.multi=body byly odstran\u011bny confirm.point.edit=bod zm\u011bn\u011bn -confirm.mergetracksegments=\u010c\u00e1sti trasy spojeny +confirm.mergetracksegments=\u010c\u00e1sti stopy spojeny confirm.reverserange=Rozmez\u00ed obr\u00e1ceno confirm.addtimeoffset=\u010casov\u00fd posun zm\u011bn\u011bn confirm.addaltitudeoffset=V\u00fd\u0161kov\u00fd posun zm\u011bn\u011bn @@ -529,7 +594,6 @@ button.cancel=Zru\u0161it button.overwrite=P\u0159epsat button.moveup=Posunout nahoru button.movedown=Posunout dol\u016f -button.showlines=Zobrazit linky m\u0159\u00ed\u017eky button.edit=Editovat button.exit=Konec button.close=Zav\u0159\u00edt @@ -552,6 +616,7 @@ button.browse=Proch\u00e1zet... button.addnew=P\u0159idat nov\u00e9 button.delete=Smazat button.manage=Upravit +button.combine=Zkombinovat # File types filetype.txt=soubory TXT @@ -562,14 +627,16 @@ filetype.kmz=soubory KMZ filetype.gpx=soubory GPX filetype.pov=soubory POV filetype.svg=soubory SVG +filetype.png=soubory PNG filetype.audio=soubory MP3, OGG, WAV # Display components display.nodata=\u017d\u00e1dn\u00e1 data -display.noaltitudes=Trasa neobsahuje informace o v\u00fd\u0161ce -display.notimestamps=Trasa neobsahuje \u010dasov\u00e9 zna\u010dky -details.trackdetails=Detaily trasy -details.notrack=\u017d\u00e1dn\u00e1 trasa +display.noaltitudes=Stopa neobsahuje informace o v\u00fd\u0161ce +display.notimestamps=Stopa neobsahuje \u010dasov\u00e9 zna\u010dky +display.novalues=Stopa neobsahuje hodnoty tohoto pole +details.trackdetails=Detaily stopy +details.notrack=\u017d\u00e1dn\u00e1 stopa details.track.points=Body details.track.file=Soubor details.track.numfiles=Po\u010det soubor\u016f @@ -638,21 +705,31 @@ units.feet=Stopy units.feet.short=stop units.kilometres=Kilometry units.kilometres.short=km +units.kilometresperhour=km za hodinu units.kilometresperhour.short=km/h units.miles=M\u00edle units.miles.short=mil +units.milesperhour=mil za hodinu units.milesperhour.short=mph units.nauticalmiles=N\u00e1mo\u0159n\u00ed m\u00edle units.nauticalmiles.short=N.m. units.nauticalmilesperhour.short=uzly +units.metrespersec=metr\u016f za sekundu units.metrespersec.short=m/s +units.feetpersec=stop za sekundu units.feetpersec.short=stop/s units.hours=hodin +units.minutes=minut +units.seconds=sekund units.degminsec=Deg-min-sec units.degmin=Deg-min units.deg=Stupn\u011b units.iso8601=ISO 8601 +# How to combine conditions, such as filters +logic.and=a +logic.or=nebo + # External urls url.googlemaps=maps.google.cz wikipedia.lang=cs @@ -672,11 +749,11 @@ undo.deletepoint=smazat bod undo.removephoto=odebrat fotografii undo.removeaudio=odebrat audionahr\u00e1vku undo.deleterange=smazat rozmez\u00ed -undo.croptrack=o\u0159\u00edznout trasu -undo.deletemarked=zkomprimovat trasu +undo.croptrack=o\u0159\u00edznout stopu +undo.deletemarked=zkomprimovat stopu undo.insert=vlo\u017eit body undo.reverse=obr\u00e1tit rozmez\u00ed -undo.mergetracksegments=slou\u010dit \u010d\u00e1sti trasy +undo.mergetracksegments=slou\u010dit \u010d\u00e1sti stopy undo.addtimeoffset=p\u0159idat \u010dasov\u00fd posun undo.addaltitudeoffset=p\u0159idat v\u00fd\u0161kov\u00fd posun undo.rearrangewaypoints=p\u0159euspo\u0159\u00e1dat body @@ -721,7 +798,7 @@ error.undofailed.text=Nepoda\u0159ilo se vr\u00e1tit operaci error.function.noop.title=Operace nic neprovedla error.rearrange.noop=P\u0159euspo\u0159\u00e1d\u00e1n\u00ed nic neprovedlo error.function.notavailable.title=Funkce nen\u00ed dostupn\u00e1 -error.function.nojava3d=Tato funkce vy\u017eaduje knihovnu Java3d,\ndostupnou na Sun.com. +error.function.nojava3d=Tato funkce vy\u017eaduje knihovnu Java3d. error.3d=P\u0159i trojrozm\u011brn\u00e9m zobrazen\u00ed do\u0161lo k chyb\u011b error.readme.notfound=Nenalezen soubor readme error.osmimage.dialogtitle=Chyba p\u0159i na\u010d\u00edt\u00e1n\u00ed mapov\u00fdch podklad\u016f @@ -738,3 +815,4 @@ error.cache.notthere=Nepoda\u0159ilo se nal\u00e9zt adres\u00e1\u0159 s cache ma error.cache.empty=Adres\u00e1\u0159 s cache map je pr\u00e1zdn\u00fd. error.cache.cannotdelete=Nelze smazat soubory map. error.interpolate.invalidparameter=Po\u010det bod\u016f mus\u00ed b\u00fdt mezi 1 a 1000 +error.learnestimationparams.failed=Na z\u00e1klad\u011b t\u00e9to stopy nelze vypo\u010d\u00edtat parametr.\nZkuste jin\u00e9 stopy. diff --git a/tim/prune/lang/prune-texts_da.properties b/tim/prune/lang/prune-texts_da.properties index b375fe3..ea394b9 100644 --- a/tim/prune/lang/prune-texts_da.properties +++ b/tim/prune/lang/prune-texts_da.properties @@ -80,6 +80,5 @@ function.show3d=3-D view function.distances=Afstande function.fullrangedetails=Vis alle detaljer function.setmapbg=V\u00e6lg kort som baggrund -function.setkmzimagesize=V\u00e6lg KMZ billedst\u00f8rrelse function.setpaths=V\u00e6lg sti til programmer function.getgpsies=Se liste af GPS-spor diff --git a/tim/prune/lang/prune-texts_de.properties b/tim/prune/lang/prune-texts_de.properties index ee57c21..4914e44 100644 --- a/tim/prune/lang/prune-texts_de.properties +++ b/tim/prune/lang/prune-texts_de.properties @@ -84,6 +84,7 @@ function.exportkml=KML exportieren function.exportgpx=GPX exportieren function.exportpov=POV exportieren function.exportsvg=SVG exportieren +function.exportimage=Bild exportieren function.editwaypointname=Name des Punkts bearbeiten function.compress=Track komprimieren function.deleterange=Bereich l\u00f6schen @@ -99,8 +100,9 @@ function.charts=Diagramme function.show3d=3D Ansicht function.distances=Entfernungen function.fullrangedetails=Zus\u00e4tzliche Bereichdetails +function.estimatetime=Zeit absch\u00e4tzen +function.learnestimationparams=Zeitparameter erlernen function.setmapbg=Karte Hintergrund setzen -function.setkmzimagesize=Bildgr\u00f6\u00dfe im KMZ setzen function.setpaths=Programmpfade setzen function.getgpsies=Tracks bei GPSies.com herunterladen function.uploadgpsies=Track zu GPSies.com hochladen @@ -159,6 +161,10 @@ dialog.openoptions.deliminfo.records=Datens\u00e4tze, mit dialog.openoptions.deliminfo.fields=Feldern dialog.openoptions.deliminfo.norecords=Keine Datens\u00e4tze dialog.openoptions.altitudeunits=Ma\u00dfeinheiten f\u00fcr die H\u00f6he +dialog.openoptions.speedunits=Ma\u00dfeinheiten f\u00fcr die Geschwindigkeiten +dialog.openoptions.vertspeedunits=Ma\u00dfeinheiten f\u00fcr vertikale Geschwindigkeiten +dialog.openoptions.vspeed.positiveup=Positive Geschwindigkeiten aufw\u00e4rts +dialog.openoptions.vspeed.positivedown=Positive Geschwindigkeiten abw\u00e4rts dialog.open.contentsdoubled=Diese Datei enth\u00e4lt zwei Kopien jedes Punkts,\neinmal als Waypoint und einmal als Trackpunkt. dialog.selecttracks.intro=W\u00e4hlen Sie den Track oder die Tracks aus, die Sie laden m\u00f6chten dialog.selecttracks.noname=Unbenannt @@ -175,7 +181,31 @@ dialog.gpsload.gettracks=Tracks laden dialog.gpsload.save=Als Datei speichern dialog.gpssend.sendwaypoints=Wegpunkte senden dialog.gpssend.sendtracks=Tracks senden -dialog.gpssend.trackname=Track Name +dialog.gpssend.trackname=Trackname +dialog.gpsbabel.filters=Filter +dialog.addfilter.title=Filter einf\u00fcgen +dialog.gpsbabel.filter.discard=Wegwerfen +dialog.gpsbabel.filter.simplify=Vereinfachen +dialog.gpsbabel.filter.distance=Distanz +dialog.gpsbabel.filter.interpolate=Interpolieren +dialog.gpsbabel.filter.discard.intro=Punkte wegwerfen, falls +dialog.gpsbabel.filter.discard.hdop=Hdop > +dialog.gpsbabel.filter.discard.vdop=Vdop > +dialog.gpsbabel.filter.discard.numsats=Anzahl Satelliten < +dialog.gpsbabel.filter.discard.nofix=Punkt kein Fix hat +dialog.gpsbabel.filter.discard.unknownfix=Punkt unbekanntes Fix hat +dialog.gpsbabel.filter.simplify.intro=Punkte entfernen bis +dialog.gpsbabel.filter.simplify.maxpoints=Anzahl Punkte < +dialog.gpsbabel.filter.simplify.maxerror=oder Fehlerdistanz < +dialog.gpsbabel.filter.simplify.crosstrack=Distanz quer +dialog.gpsbabel.filter.simplify.length=L\u00e4ngendifferenz +dialog.gpsbabel.filter.simplify.relative=Relativ zum Hdop +dialog.gpsbabel.filter.distance.intro=Punkte entfernen, die in der N\u00e4he von fr\u00fcheren Punkten sind +dialog.gpsbabel.filter.distance.distance=Falls Distanz < +dialog.gpsbabel.filter.distance.time=und Zeitdifferenz < +dialog.gpsbabel.filter.interpolate.intro=Zus\u00e4tzliche Punkte hineinf\u00fcgen +dialog.gpsbabel.filter.interpolate.distance=Falls Distanz > +dialog.gpsbabel.filter.interpolate.time=oder Zeitdifferenz > dialog.saveoptions.title=Datei speichern dialog.save.fieldstosave=Zu speichernde Felder dialog.save.table.field=Feld @@ -192,7 +222,10 @@ dialog.exportkml.text=Titel f\u00fcr die Daten dialog.exportkml.altitude=Absolute H\u00f6heninformation (f\u00fcr Luftfahrt) dialog.exportkml.kmz=Daten in KMZ-Datei komprimieren dialog.exportkml.exportimages=Vorschaubilder mit in KMZ-Datei exportieren +dialog.exportkml.imagesize=Bildgr\u00f6\u00dfe dialog.exportkml.trackcolour=Trackfarbe +dialog.exportkml.standardkml=Standardes KML +dialog.exportkml.extendedkml=Erweitertes KML mit Zeitstempeln dialog.exportgpx.name=Name dialog.exportgpx.desc=Beschreibung dialog.exportgpx.includetimestamps=Zeitstempel mit exportieren @@ -208,11 +241,23 @@ dialog.exportpov.cameraz=Kamera Z dialog.exportpov.modelstyle=Modellstil dialog.exportpov.ballsandsticks=B\u00e4lle und Stangen dialog.exportpov.tubesandwalls=R\u00f6hren und W\u00e4nde -dialog.exportpov.warningtracksize=Dieser Track hat sehr viele Punkte, die Java3D vielleicht nicht bearbeiten kann.\nM\u00f6chten Sie den Vorgang trotzdem fortsetzen? +dialog.3d.warningtracksize=Dieser Track hat sehr viele Punkte, die Java3D vielleicht nicht bearbeiten kann.\nM\u00f6chten Sie den Vorgang trotzdem fortsetzen? +dialog.exportpov.baseimage=Grundbild +dialog.exportpov.cannotmakebaseimage=Bild kann nicht gespeichert werden +dialog.baseimage.title=Kartenbild +dialog.baseimage.useimage=Bild verwenden +dialog.baseimage.mapsource=Kartenquelle +dialog.baseimage.zoom=Zoomstufe +dialog.baseimage.incomplete=Bild unvollst\u00e4ndig +dialog.baseimage.tiles=Kacheln +dialog.baseimage.size=Bildgr\u00f6\u00dfe dialog.exportsvg.text=W\u00e4hlen Sie die Parameter f\u00fcr den SVG-Export aus dialog.exportsvg.phi=Richtungswinkel \u03d5 dialog.exportsvg.theta=Neigungswinkel \u03b8 dialog.exportsvg.gradients=Farbverl\u00e4ufe verwenden +dialog.exportimage.noimagepossible=Kartenbilder m\u00fcssen schon gespeichert werden bevor sie in einem Export verwendet werden k\u00f6nnen +dialog.exportimage.drawtrack=Track auf der Karte zeichnen +dialog.exportimage.textscalepercent=Text Skalierung (%) dialog.pointtype.desc=Folgende Punkttypen speichern: dialog.pointtype.track=Trackpunkte dialog.pointtype.waypoint=Wegpunkte @@ -233,7 +278,9 @@ dialog.clearundo.title=Undo-Liste l\u00f6schen dialog.clearundo.text=Wollen Sie wirklich die Undo-Liste l\u00f6schen?\nAlle Undo- Informationen werden verloren gehen! dialog.pointedit.title=Punkt bearbeiten dialog.pointedit.text=W\u00e4hlen Sie die Felder aus, die Sie bearbeiten m\u00f6chten, und verwenden Sie den 'Bearbeiten'-Button, um den Wert zu \u00e4ndern +dialog.pointedit.intro=W\u00e4hlen Sie die Felder aus um die Werte zu sehen und bearbeiten dialog.pointedit.table.field=Feld +dialog.pointedit.nofield=Keinen Feld ausgew\u00e4hlt dialog.pointedit.table.value=Wert dialog.pointedit.table.changed=Ge\u00e4ndert dialog.pointedit.changevalue.text=Geben Sie den neuen Wert f\u00fcr dieses Feld ein @@ -279,6 +326,27 @@ dialog.distances.toofewpoints=Diese Funktion braucht Wegpunkte, um die Distanzen dialog.fullrangedetails.intro=Detaillierte Angaben zum markierten Bereich dialog.fullrangedetails.coltotal=Mit L\u00fccken dialog.fullrangedetails.colsegments=Ohne L\u00fccken +dialog.estimatetime.details=Details +dialog.estimatetime.gentle=Leicht +dialog.estimatetime.steep=Steil +dialog.estimatetime.climb=Aufstieg +dialog.estimatetime.descent=Abstieg +dialog.estimatetime.parameters=Parameter +dialog.estimatetime.parameters.timefor=Zeit f\u00fcr +dialog.estimatetime.results=Ergebnisse +dialog.estimatetime.results.estimatedtime=Abgesch\u00e4tzte Zeit +dialog.estimatetime.results.actualtime=Gebrauchte Zeit +dialog.estimatetime.error.nodistance=Die Absch\u00e4tzungen brauchen zusammengebundete Punkte mit einer Distanz +dialog.estimatetime.error.noaltitudes=Der Bereich enth\u00e4lt keine H\u00f6heninformation +dialog.learnestimationparams.intro=Hier sind die Parameter die aus diesem Track berechnet wurden +dialog.learnestimationparams.averageerror=Fehler +dialog.learnestimationparams.combine=Diese Parameter k\u00f6nnen mit den aktuellen Werten zusammengeschlossen werden +dialog.learnestimationparams.combinedresults=Zusammengeschlossenen Ergebnisse +dialog.learnestimationparams.weight.100pccurrent=Aktuelle Werte behalten +dialog.learnestimationparams.weight.current=aktuell +dialog.learnestimationparams.weight.calculated=berechnet +dialog.learnestimationparams.weight.50pc=Mittelwert zwischen aktuellen und berechneten Werten +dialog.learnestimationparams.weight.100pccalculated=Neue berechnete Werte \u00fcbernehmen dialog.setmapbg.intro=Eine der Quellen ausw\u00e4hlen oder eine neue hinzuf\u00fcgen dialog.addmapsource.title=Neue Kartenquelle hinzuf\u00fcgen dialog.addmapsource.sourcename=Name der Quelle @@ -422,8 +490,7 @@ dialog.saveconfig.prune.exiftoolpath=ExifTool-Pfad dialog.saveconfig.prune.mapsource=Kartenserver-Index dialog.saveconfig.prune.mapsourcelist=Kartenserver dialog.saveconfig.prune.diskcache=Kartenordner -dialog.saveconfig.prune.kmzimagewidth=Bildbreite in KMZ -dialog.saveconfig.prune.kmzimageheight=Bildh\u00f6he in KMZ +dialog.saveconfig.prune.kmzimagewidth=Bildgr\u00f6\u00dfe in KMZ dialog.saveconfig.prune.colourscheme=Farbschema dialog.saveconfig.prune.linewidth=Liniedicke dialog.saveconfig.prune.kmltrackcolour=KML-Trackfarbe @@ -478,9 +545,6 @@ dialog.searchwikipedianames.search=Suche nach: # 3d window dialog.3d.title=GpsPrune-3D-Ansicht dialog.3d.altitudefactor=Vervielfachungsfaktor f\u00fcr H\u00f6hen -dialog.3dlines.title=GpsPrune-Gitterlinien -dialog.3dlines.empty=Keine Linien zum Anzeigen! -dialog.3dlines.intro=Hier sind die Linien f\u00fcr die 3D Ansicht # Confirm messages confirm.loadfile=Daten aus Datei geladen @@ -529,7 +593,6 @@ button.cancel=Abbrechen button.overwrite=\u00dcberschreiben button.moveup=Nach oben button.movedown=Nach unten -button.showlines=Linien anzeigen button.edit=Bearbeiten button.exit=Beenden button.close=Schlie\u00dfen @@ -552,6 +615,7 @@ button.browse=Durchsuchen... button.addnew=Hinzuf\u00fcgen button.delete=Entfernen button.manage=Verwalten +button.combine=Zusammenschlie\u00dfen # File types filetype.txt=TXT-Dateien @@ -562,12 +626,14 @@ filetype.kmz=KMZ-Dateien filetype.gpx=GPX-Dateien filetype.pov=POV-Dateien filetype.svg=SVG-Dateien +filetype.png=PNG-Dateien filetype.audio=MP3-, OGG-, WAV-Dateien # Display components display.nodata=Keine Daten geladen display.noaltitudes=Track enth\u00e4lt keine H\u00f6henangaben display.notimestamps=Track enth\u00e4lt keine Zeitstempel +display.novalues=Track enth\u00e4lt keine Daten f\u00fcr dieses Feld details.trackdetails=Details des Tracks details.notrack=Kein Track geladen details.track.points=Punkte @@ -636,18 +702,31 @@ units.metres=Meter units.metres.short=m units.kilometres=Kilometer units.kilometres.short=km +units.kilometresperhour=km pro Stunde units.kilometresperhour.short=km/h +units.miles=Meilen +units.miles.short=Mi +units.milesperhour=Meilen pro Stunde +units.milesperhour.short=mph units.nauticalmiles=Seemeilen units.nauticalmiles.short=sm units.nauticalmilesperhour.short=kn +units.metrespersec=Meter pro Sekunde units.metrespersec.short=m/s +units.feetpersec=feet pro Sekunde units.feetpersec.short=ft/s units.hours=Std +units.minutes=Minuten +units.seconds=Sekunden units.degminsec=Grad-Min-Sek units.degmin=Grad-Min units.deg=Grad units.iso8601=ISO 8601 +# How to combine conditions, such as filters +logic.and=und +logic.or=oder + # External urls url.googlemaps=maps.google.de wikipedia.lang=de @@ -716,7 +795,7 @@ error.undofailed.text=Operation konnte nicht r\u00fcckg\u00e4ngig gemacht werden error.function.noop.title=Funktion hat nichts bewirkt error.rearrange.noop=Die Neuanordnung der Punkte hatte keinen Effekt error.function.notavailable.title=Funktion nicht verf\u00fcgbar -error.function.nojava3d=Diese Funktion ben\u00f6tigt die Java3d-Library,\ndie bei Sun.com erh\u00e4ltlich ist. +error.function.nojava3d=Diese Funktion ben\u00f6tigt die Java3d-Library. error.3d=Ein Fehler ist bei der 3D Darstellung aufgetreten error.readme.notfound=Liesmich-Datei nicht gefunden error.osmimage.dialogtitle=Laden von Karten-Bildern fehlgeschlagen @@ -733,3 +812,4 @@ error.cache.notthere=Der Ordner wurde nicht gefunden error.cache.empty=Der Ordner ist leer error.cache.cannotdelete=Es konnte keine Kacheln gel\u00f6scht werden error.interpolate.invalidparameter=Die Anzahl der Punkte muss zwischen 1 und 1000 liegen +error.learnestimationparams.failed=Mit diesem Track k\u00f6nnen die Parameter nicht berechnet werden.\nVersuchen Sie mit mehreren Tracks. diff --git a/tim/prune/lang/prune-texts_de_CH.properties b/tim/prune/lang/prune-texts_de_CH.properties index caa5bd6..fa2931f 100644 --- a/tim/prune/lang/prune-texts_de_CH.properties +++ b/tim/prune/lang/prune-texts_de_CH.properties @@ -83,6 +83,7 @@ function.exportkml=KML exportier\u00e4 function.exportgpx=GPX exportier\u00e4 function.exportpov=POV exportier\u00e4 function.exportsvg=SVG exportier\u00e4 +function.exportimage=Bild exportier\u00e4 function.editwaypointname=Waypoint Name editiere function.compress=Track komprimier\u00e4 function.deleterange=Beriich l\u00f6sche @@ -97,6 +98,8 @@ function.charts=Diagramme function.show3d=Dr\u00fc\u00fc-D Aasicht function.distances=Entf\u00e4rnige function.fullrangedetails=Zues\u00e4tzlichi Beriichinfos +function.estimatetime=Ziit absch\u00e4tze +function.learnestimationparams=Ziitparameter erlerne function.setmapbg=Karte Hintegrund setz\u00e4 function.getgpsies=Gpsies Tracks hol\u00e4 function.uploadgpsies=Date zum Gpsies uufalad\u00e4 @@ -116,7 +119,6 @@ function.removeaudio=Audiodatei entfern\u00e4 function.correlateaudios=Audios korrelier\u00e4 function.playaudio=Audiofile abspiel\u00e4 function.stopaudio=Abspielen abbr\u00e4ch\u00e4 -function.setkmzimagesize=Bildligr\u00f6sse inem KMZ setz\u00e4 function.setpaths=Programmepfade setz\u00e4 function.setcolours=Farben setz\u00e4 function.setlinewidth=Liniedicke setz\u00e4 @@ -154,6 +156,10 @@ dialog.openoptions.deliminfo.records=Rekords, mit dialog.openoptions.deliminfo.fields=F\u00e4ldere dialog.openoptions.deliminfo.norecords=Kei Rekords dialog.openoptions.altitudeunits=H\u00f6chi Masseiheite +dialog.openoptions.speedunits=Masseiheite f\u00fcr Geschwindigkeite +dialog.openoptions.vertspeedunits=Masseiheite f\u00fcr vertikale Geschwindigkeite +dialog.openoptions.vspeed.positiveup=Positive bed\u00fc\u00fctet uufe +dialog.openoptions.vspeed.positivedown=Positive bed\u00fc\u00fctet abe dialog.open.contentsdoubled=Dieses File h\u00e4t zwei Kopien von j\u00e4dem Punkt,\neimol als Waypoint und eimol als Trackpunkt. dialog.selecttracks.intro=W\u00e4hlet Sie die Tracks uus zum lad\u00e4 dialog.selecttracks.noname=Unbenannt @@ -172,6 +178,30 @@ dialog.gpssend.sendwaypoints=Waypoints schicke dialog.gpssend.sendtracks=Tracks schicke dialog.gpssend.trackname=Track Name dialog.saveoptions.title=File speicher\u00e4 +dialog.gpsbabel.filters=Filter +dialog.addfilter.title=Filter inna\u00fce +dialog.gpsbabel.filter.discard=W\u00e4gwerfe +dialog.gpsbabel.filter.simplify=Vereifache +dialog.gpsbabel.filter.distance=Distanz +dialog.gpsbabel.filter.interpolate=Interpoliere +dialog.gpsbabel.filter.discard.intro=P\u00fcnkte wegwerfen, im Fall +dialog.gpsbabel.filter.discard.hdop=Hdop > +dialog.gpsbabel.filter.discard.vdop=Vdop > +dialog.gpsbabel.filter.discard.numsats=Aazahl Satellite < +dialog.gpsbabel.filter.discard.nofix=Punkt kei Fix h\u00e4t +dialog.gpsbabel.filter.discard.unknownfix=Punkt unbekannti Fix h\u00e4t +dialog.gpsbabel.filter.simplify.intro=P\u00fcnkte entf\u00e4rne bis +dialog.gpsbabel.filter.simplify.maxpoints=Aazahl P\u00fcnkte < +dialog.gpsbabel.filter.simplify.maxerror=oder F\u00e4hlerdistanz < +dialog.gpsbabel.filter.simplify.crosstrack=Distanz quer +dialog.gpsbabel.filter.simplify.length=L\u00e4ngediffer\u00e4nz +dialog.gpsbabel.filter.simplify.relative=Relativ zum Hdop +dialog.gpsbabel.filter.distance.intro=P\u00fcnkte entf\u00e4rne, die in der N\u00f6chi vo fr\u00fchere P\u00fcnkte sin +dialog.gpsbabel.filter.distance.distance=im Fall Distanz < +dialog.gpsbabel.filter.distance.time=und Ziitdiffer\u00e4nz < +dialog.gpsbabel.filter.interpolate.intro=Zus\u00e4tzlichi P\u00fcnkte innat\u00fce +dialog.gpsbabel.filter.interpolate.distance=im Fall Distanz > +dialog.gpsbabel.filter.interpolate.time=oder Ziitdiffer\u00e4nz > dialog.save.fieldstosave=F\u00e4lder zu speicher\u00e4 dialog.save.table.field=F\u00e4ld dialog.save.table.hasdata=Het Date @@ -187,7 +217,10 @@ dialog.exportkml.text=Titel f\u00fcr die Date dialog.exportkml.altitude=Absolut H\u00f6chiinformation (f\u00fcrs Fliege) dialog.exportkml.kmz=Date ins kmz File komprimier\u00e4 dialog.exportkml.exportimages=Bildli ins Kmz exportier\u00e4 +dialog.exportkml.imagesize=Bildligr\u00f6sse dialog.exportkml.trackcolour=Trackfarb +dialog.exportkml.standardkml=Standardes KML +dialog.exportkml.extendedkml=Erwiitertes KML mit Ziitst\u00e4mple dialog.exportgpx.name=Name dialog.exportgpx.desc=Beschriibig dialog.exportgpx.includetimestamps=Au Ziitst\u00e4mpel @@ -203,11 +236,23 @@ dialog.exportpov.cameraz=Kamera Z dialog.exportpov.modelstyle=Modellstil dialog.exportpov.ballsandsticks=B\u00e4lle und Schtange dialog.exportpov.tubesandwalls=R\u00f6hre und W\u00e4nde -dialog.exportpov.warningtracksize=Dieser Track h\u00e4t mega viele P\u00fcnkte, die Java3D villiicht n\u00f6d chann bearbeite.\nSind Sie sicher, Sie wend trotzdem fortsetze? +dialog.3d.warningtracksize=Dieser Track h\u00e4t mega viele P\u00fcnkte, die Java3D villiicht n\u00f6d chann bearbeite.\nSind Sie sicher, Sie wend trotzdem fortsetze? +dialog.exportpov.baseimage=Grundbild +dialog.exportpov.cannotmakebaseimage=Bild chann n\u00f6d gspeicheret werde +dialog.baseimage.title=Kartenbild +dialog.baseimage.useimage=Bild verw\u00e4nde +dialog.baseimage.mapsource=Kartequ\u00e4lle +dialog.baseimage.zoom=Zoomstufe +dialog.baseimage.incomplete=Bild unvollst\u00e4ndig +dialog.baseimage.tiles=Kachle +dialog.baseimage.size=Bildgr\u00f6ssi dialog.exportsvg.text=W\u00e4hlet Sie die Parameter f\u00fcrs SVG Export uus dialog.exportsvg.phi=Richtigswinkel \u03D5 dialog.exportsvg.theta=Neigigswinkel \u03B8 dialog.exportsvg.gradients=Farbeverl\u00e4ufe verw\u00e4nde +dialog.exportimage.noimagepossible=Kartebilder m\u00fcsset scho gspeicheret werde, bevor sie bim Export verwendet werde k\u00f6nne +dialog.exportimage.drawtrack=Track uf d Karte zeichne +dialog.exportimage.textscalepercent=Text Skalierig (%) dialog.pointtype.desc=Folgende Punkttype speichere: dialog.pointtype.track=Trackp\u00fcnkte dialog.pointtype.waypoint=Waypoints @@ -228,7 +273,9 @@ dialog.clearundo.title=Undo-Liste l\u00f6sch\u00e4 dialog.clearundo.text=Sind Sie sicher, Sie wend die Undo-Liste l\u00f6sche?\nAlle Undo Infos werdet verlore gah! dialog.pointedit.title=Punkt editier\u00e4 dialog.pointedit.text=W\u00e4hlet Sie j\u00e4den F\u00e4ld uus zu editiere, und mitem 'Editier\u00e4' Chnopf den Wert \u00e4ndere +dialog.pointedit.intro=W\u00e4hlet Sie j\u00e4den F\u00e4ld uus, um den Wert z'seh und z'\u00e4ndere dialog.pointedit.table.field=F\u00e4ld +dialog.pointedit.nofield=Kei F\u00e4ld uusgew\u00e4hlt dialog.pointedit.table.value=Wert dialog.pointedit.table.changed=Ge\u00e4ndert dialog.pointedit.changevalue.text=Gebet Sie den neuen Wert f\u00fcr diesen F\u00e4ld ina @@ -274,6 +321,27 @@ dialog.distances.toofewpoints=d'Funktion bruucht Waypoints um die Dischtanze z b dialog.fullrangedetails.intro=Hier sind die Infos vonem aktuelli Beriich dialog.fullrangedetails.coltotal=Inklusiv L\u00fccke dialog.fullrangedetails.colsegments=Ohni L\u00fccke +dialog.estimatetime.details=Details +dialog.estimatetime.gentle=Liecht +dialog.estimatetime.steep=Steil +dialog.estimatetime.climb=Uufstieg +dialog.estimatetime.descent=Abstieg +dialog.estimatetime.parameters=Parameter +dialog.estimatetime.parameters.timefor=Ziit f\u00fcr +dialog.estimatetime.results=Resultate +dialog.estimatetime.results.estimatedtime=Abgesch\u00e4tzti Ziit +dialog.estimatetime.results.actualtime=Gebruuchti Ziit +dialog.estimatetime.error.nodistance=D Absch\u00e4tzige bruuchet zamegebundeti P\u00fcnkt mitene Distanz +dialog.estimatetime.error.noaltitudes=D Beriich h\u00e4t kei H\u00f6hi Date +dialog.learnestimationparams.intro=Hier sin die Parameter die usem Track uusgr\u00e4chnet worde sin +dialog.learnestimationparams.averageerror=Fehler +dialog.learnestimationparams.combine=Diese Parameter k\u00f6nnet miten aktuelli Werte z\u00e4megschlosse werde +dialog.learnestimationparams.combinedresults=Z\u00e4megschlossene Resultate +dialog.learnestimationparams.weight.100pccurrent=Aktuelli Werte behalte +dialog.learnestimationparams.weight.current=aktuell +dialog.learnestimationparams.weight.calculated=uusgr\u00e4chnet +dialog.learnestimationparams.weight.50pc=Mittelwert zw\u00fcschet aktuelli und uusgr\u00e4chneti Werte +dialog.learnestimationparams.weight.100pccalculated=Neui uusgr\u00e4chneti Werte \u00fcberneh dialog.setmapbg.intro=Eini von den Qu\u00e4llen uusw\u00e4hle, oder eini neui hinzuef\u00fcge dialog.addmapsource.title=Neui Kartequ\u00e4lle hinzuef\u00fcge dialog.addmapsource.sourcename=Sourcename @@ -417,8 +485,7 @@ dialog.saveconfig.prune.exiftoolpath=Exiftool Pfad dialog.saveconfig.prune.mapsource=Kartenserver Index dialog.saveconfig.prune.mapsourcelist=Kartenservers dialog.saveconfig.prune.diskcache=Kartenordner -dialog.saveconfig.prune.kmzimagewidth=Bildbreiti im KMZ -dialog.saveconfig.prune.kmzimageheight=Bildh\u00f6chi im KMZ +dialog.saveconfig.prune.kmzimagewidth=Bildr\u00f6sse im KMZ dialog.saveconfig.prune.colourscheme=Farbeschema dialog.saveconfig.prune.linewidth=Liniedicke dialog.saveconfig.prune.kmltrackcolour=KML Trackfarb @@ -473,9 +540,6 @@ dialog.searchwikipedianames.search=Sueche na: # 3d window dialog.3d.title=GpsPrune Dr\u00fc\u00fc-d Aasicht dialog.3d.altitudefactor=H\u00f6chivervilfachigsfaktor -dialog.3dlines.title=GpsPrune Gitterlinie -dialog.3dlines.empty=Kei Linie zum aazeig\u00e4! -dialog.3dlines.intro=Hier sin die Linie f\u00fcr die dr\u00fc\u00fc-D Aasicht # Confirm messages confirm.loadfile=Date glade vom @@ -525,7 +589,6 @@ button.cancel=Abbr\u00e4ch\u00e4 button.overwrite=Überschriib\u00e4 button.moveup=Uuf\u00e4 schieb\u00e4 button.movedown=Aba schieb\u00e4 -button.showlines=Linie aazeig\u00e4 button.edit=Editier\u00e4 button.exit=Be\u00e4nd\u00e4 button.close=Schliess\u00e4 @@ -548,6 +611,7 @@ button.browse=Durasuech\u00e4... button.addnew=Hinzuef\u00fcg\u00e4 button.delete=Entf\u00e4rn\u00e4 button.manage=Verwolt\u00e4 +button.combine=Z\u00e4meschliess\u00e4 # File types filetype.txt=TXT Dateie @@ -558,12 +622,14 @@ filetype.kmz=KMZ Dateie filetype.gpx=GPX Dateie filetype.pov=POV Dateie filetype.svg=SVG Dateie +filetype.png=PNG Dateie filetype.audio=MP3, OGG, WAV Dateie # Display components display.nodata=Kei Date glade worde display.noaltitudes=Track h\u00e4t kei H\u00f6hi Date display.notimestamps=Track h\u00e4t kei Ziitst\u00e4mple +display.novalues=Track h\u00e4t kei Date f\u00fcr s'F\u00e4ld details.trackdetails=Details vom Track details.notrack=Kei Track glade worde details.track.points=P\u00fcnkte @@ -631,18 +697,31 @@ units.metres=Meter units.metres.short=m units.kilometres=Kilometer units.kilometres.short=km +units.kilometresperhour=km pro Stund units.kilometresperhour.short=kmh +units.miles=Meile +units.miles.short=Mi +units.milesperhour=Meile pro Stund +units.milesperhour.short=mph units.nauticalmiles=Seemeile units.nauticalmiles.short=sm units.nauticalmilesperhour.short=kn +units.metrespersec=Meter pro Sekunde units.metrespersec.short=m/s +units.feetpersec=feet pro Sekunde units.feetpersec.short=ft/s units.hours=Std +units.minutes=Minute +units.seconds=Sekunde units.degminsec=Grad-Min-Sek units.degmin=Grad-Min units.deg=Grad units.iso8601=ISO 8601 +# How to combine conditions, such as filters +logic.and=und +logic.or=oder + # External urls url.googlemaps=maps.google.ch wikipedia.lang=als @@ -711,7 +790,7 @@ error.undofailed.text=Operation kann n\u00f6d r\u00fcckg\u00e4ngig gmacht werde error.function.noop.title=Funktion h\u00e4t gar n\u00fc\u00fct gmacht error.rearrange.noop=P\u00fcnkte Reorganisierig h\u00e4t kei Eff\u00e4kt gha error.function.notavailable.title=Funktion n\u00f6d verf\u00fcegbar -error.function.nojava3d=Sorry, d'Funktion brucht d Java3d Library,\nvo Sun.com erh\u00e4ltlech. +error.function.nojava3d=Sorry, d'Funktion brucht d Java3d Library. error.3d=N F\u00e4hler isch mitere 3d Darstellig ufgtr\u00e4te error.readme.notfound=L\u00e4s mi File n\u00f6d gfunde error.osmimage.dialogtitle=F\u00e4hle bim Bildli-Lade @@ -728,3 +807,4 @@ error.cache.notthere=D Ordner isch n\u00f6d gfunde worde error.cache.empty=D Ordner h\u00e4t n\u00fc\u00fct drinne error.cache.cannotdelete=Es sin kei Kachle gl\u00f6scht worde error.interpolate.invalidparameter=D'Aazahl P\u00fcnkt muess zw\u00fcschet 1 und 1000 sii +error.learnestimationparams.failed=Mit dere Track k\u00f6nnet die Parameter n\u00f6d br\u00e4chnet werde.\nVersuechet Sie mit mehreri Tracks. diff --git a/tim/prune/lang/prune-texts_en.properties b/tim/prune/lang/prune-texts_en.properties index f2e70e1..0193cb8 100644 --- a/tim/prune/lang/prune-texts_en.properties +++ b/tim/prune/lang/prune-texts_en.properties @@ -84,6 +84,7 @@ function.exportkml=Export KML function.exportgpx=Export GPX function.exportpov=Export POV function.exportsvg=Export SVG +function.exportimage=Export image function.editwaypointname=Edit waypoint name function.compress=Compress track function.deleterange=Delete range @@ -99,6 +100,8 @@ function.charts=Charts function.show3d=Three-D view function.distances=Distances function.fullrangedetails=Full range details +function.estimatetime=Estimate time +function.learnestimationparams=Learn time estimation parameters function.getgpsies=Get Gpsies tracks function.uploadgpsies=Upload track to Gpsies function.lookupsrtm=Get altitudes from SRTM @@ -121,7 +124,6 @@ function.correlateaudios=Correlate audios function.playaudio=Play audio clip function.stopaudio=Stop audio clip function.setmapbg=Set map background -function.setkmzimagesize=Set KMZ image size function.setpaths=Set program paths function.setcolours=Set colours function.setlinewidth=Set line width @@ -159,6 +161,10 @@ dialog.openoptions.deliminfo.records=records, with dialog.openoptions.deliminfo.fields=fields dialog.openoptions.deliminfo.norecords=No records dialog.openoptions.altitudeunits=Altitude units +dialog.openoptions.speedunits=Speed units +dialog.openoptions.vertspeedunits=Vertical speed units +dialog.openoptions.vspeed.positiveup=Positive speeds upwards +dialog.openoptions.vspeed.positivedown=Positive speeds downwards dialog.open.contentsdoubled=This file contains two copies of each point,\nonce as waypoints and once as track points. dialog.selecttracks.intro=Select the track or tracks to load dialog.selecttracks.noname=Unnamed @@ -176,6 +182,30 @@ dialog.gpsload.save=Save to file dialog.gpssend.sendwaypoints=Send waypoints dialog.gpssend.sendtracks=Send tracks dialog.gpssend.trackname=Track name +dialog.gpsbabel.filters=Filters +dialog.addfilter.title=Add filter +dialog.gpsbabel.filter.discard=Discard +dialog.gpsbabel.filter.simplify=Simplify +dialog.gpsbabel.filter.distance=Distance +dialog.gpsbabel.filter.interpolate=Interpolate +dialog.gpsbabel.filter.discard.intro=Discard points if +dialog.gpsbabel.filter.discard.hdop=Hdop > +dialog.gpsbabel.filter.discard.vdop=Vdop > +dialog.gpsbabel.filter.discard.numsats=Number of satellites < +dialog.gpsbabel.filter.discard.nofix=Point has no fix +dialog.gpsbabel.filter.discard.unknownfix=Point has unknown fix +dialog.gpsbabel.filter.simplify.intro=Remove points until +dialog.gpsbabel.filter.simplify.maxpoints=Number of points < +dialog.gpsbabel.filter.simplify.maxerror=or error distance < +dialog.gpsbabel.filter.simplify.crosstrack=cross-track +dialog.gpsbabel.filter.simplify.length=length difference +dialog.gpsbabel.filter.simplify.relative=relative to hdop +dialog.gpsbabel.filter.distance.intro=Remove points if close to any previous point +dialog.gpsbabel.filter.distance.distance=If distance < +dialog.gpsbabel.filter.distance.time=and time difference < +dialog.gpsbabel.filter.interpolate.intro=Add extra points between track points +dialog.gpsbabel.filter.interpolate.distance=If distance > +dialog.gpsbabel.filter.interpolate.time=or time difference > dialog.saveoptions.title=Save file dialog.save.fieldstosave=Fields to save dialog.save.table.field=Field @@ -192,7 +222,10 @@ dialog.exportkml.text=Title for the data dialog.exportkml.altitude=Absolute altitudes (for aviation) dialog.exportkml.kmz=Compress to make kmz file dialog.exportkml.exportimages=Export image thumbnails to kmz +dialog.exportkml.imagesize=Image size dialog.exportkml.trackcolour=Track colour +dialog.exportkml.standardkml=Standard KML +dialog.exportkml.extendedkml=Extended KML with timestamps dialog.exportgpx.name=Name dialog.exportgpx.desc=Description dialog.exportgpx.includetimestamps=Include timestamps @@ -208,11 +241,23 @@ dialog.exportpov.cameraz=Camera Z dialog.exportpov.modelstyle=Model style dialog.exportpov.ballsandsticks=Balls and sticks dialog.exportpov.tubesandwalls=Tubes and walls -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.3d.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.baseimage=Base image +dialog.exportpov.cannotmakebaseimage=Cannot write base image +dialog.baseimage.title=Map image +dialog.baseimage.useimage=Use image +dialog.baseimage.mapsource=Map source +dialog.baseimage.zoom=Zoom level +dialog.baseimage.incomplete=Image incomplete +dialog.baseimage.tiles=Tiles +dialog.baseimage.size=Image size dialog.exportsvg.text=Select the parameters for the SVG export dialog.exportsvg.phi=Azimuth angle \u03D5 dialog.exportsvg.theta=Elevation angle \u03B8 dialog.exportsvg.gradients=Use gradients for shading +dialog.exportimage.noimagepossible=Map images need to be cached to disk in order to use them for an export. +dialog.exportimage.drawtrack=Draw track on map +dialog.exportimage.textscalepercent=Text scale factor (%) dialog.pointtype.desc=Save the following point types: dialog.pointtype.track=Track points dialog.pointtype.waypoint=Waypoints @@ -232,12 +277,14 @@ 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.text= +dialog.pointedit.intro=Select each field in turn to view and change the value dialog.pointedit.table.field=Field +dialog.pointedit.nofield=No field selected 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.pointedit.table.changed= +dialog.pointedit.changevalue.text= +dialog.pointedit.changevalue.title= dialog.pointnameedit.name=Waypoint name dialog.pointnameedit.uppercase=UPPER case dialog.pointnameedit.lowercase=lower case @@ -279,6 +326,27 @@ dialog.distances.toofewpoints=This function needs waypoints in order to calculat dialog.fullrangedetails.intro=Here are the details for the selected range dialog.fullrangedetails.coltotal=Including gaps dialog.fullrangedetails.colsegments=Without gaps +dialog.estimatetime.details=Details +dialog.estimatetime.gentle=Gentle +dialog.estimatetime.steep=Steep +dialog.estimatetime.climb=Climb +dialog.estimatetime.descent=Descent +dialog.estimatetime.parameters=Parameters +dialog.estimatetime.parameters.timefor=Time for +dialog.estimatetime.results=Results +dialog.estimatetime.results.estimatedtime=Estimated time +dialog.estimatetime.results.actualtime=Actual time +dialog.estimatetime.error.nodistance=The time estimates need connected track points, to give a distance +dialog.estimatetime.error.noaltitudes=The selection doesn't include any altitude information +dialog.learnestimationparams.intro=These are the parameters calculated from this track +dialog.learnestimationparams.averageerror=Average error +dialog.learnestimationparams.combine=These parameters can be combined with the current values +dialog.learnestimationparams.combinedresults=Combined results +dialog.learnestimationparams.weight.100pccurrent=Keep current values +dialog.learnestimationparams.weight.current=current +dialog.learnestimationparams.weight.calculated=calculated +dialog.learnestimationparams.weight.50pc=Average of current values and calculated ones +dialog.learnestimationparams.weight.100pccalculated=Use new calculated values dialog.setmapbg.intro=Select one of the map sources, or add a new one dialog.addmapsource.title=Add new map source dialog.addmapsource.sourcename=Name of source @@ -422,8 +490,7 @@ dialog.saveconfig.prune.exiftoolpath=Path to exiftool dialog.saveconfig.prune.mapsource=Selected map source dialog.saveconfig.prune.mapsourcelist=Map sources dialog.saveconfig.prune.diskcache=Map cache -dialog.saveconfig.prune.kmzimagewidth=KMZ image width -dialog.saveconfig.prune.kmzimageheight=KMZ image height +dialog.saveconfig.prune.kmzimagewidth=KMZ image size dialog.saveconfig.prune.colourscheme=Colour scheme dialog.saveconfig.prune.linewidth=Line width dialog.saveconfig.prune.kmltrackcolour=KML track colour @@ -478,9 +545,6 @@ dialog.searchwikipedianames.search=Search for: # 3d window dialog.3d.title=GpsPrune Three-d view dialog.3d.altitudefactor=Altitude exaggeration factor -dialog.3dlines.title=GpsPrune gridlines -dialog.3dlines.empty=No gridlines to display! -dialog.3dlines.intro=These are the gridlines for the three-d view # Confirm messages confirm.loadfile=Data loaded from file @@ -529,7 +593,6 @@ button.cancel=Cancel button.overwrite=Overwrite button.moveup=Move up button.movedown=Move down -button.showlines=Show lines button.edit=Edit button.exit=Exit button.close=Close @@ -552,6 +615,7 @@ button.browse=Browse... button.addnew=Add new button.delete=Delete button.manage=Manage +button.combine=Combine # File types filetype.txt=TXT files @@ -562,12 +626,14 @@ filetype.kmz=KMZ files filetype.gpx=GPX files filetype.pov=POV files filetype.svg=SVG files +filetype.png=PNG files filetype.audio=MP3, OGG, WAV files # Display components display.nodata=No data loaded display.noaltitudes=Track data does not include altitudes display.notimestamps=Track data does not include timestamps +display.novalues=Track data does not include values for this field details.trackdetails=Track details details.notrack=No track loaded details.track.points=Points @@ -638,21 +704,31 @@ units.feet=Feet units.feet.short=ft units.kilometres=Kilometres units.kilometres.short=km +units.kilometresperhour=km per hour units.kilometresperhour.short=km/h units.miles=Miles units.miles.short=mi +units.milesperhour=miles per hour units.milesperhour.short=mph units.nauticalmiles=Nautical miles units.nauticalmiles.short=N.m. units.nauticalmilesperhour.short=kts +units.metrespersec=metres per second units.metrespersec.short=m/s +units.feetpersec=feet per second units.feetpersec.short=ft/s units.hours=hours +units.minutes=minutes +units.seconds=seconds units.degminsec=Deg-min-sec units.degmin=Deg-min units.deg=Degrees units.iso8601=ISO 8601 +# How to combine conditions, such as filters +logic.and=and +logic.or=or + # External urls url.googlemaps=maps.google.co.uk wikipedia.lang=en @@ -721,7 +797,7 @@ error.undofailed.text=Failed to undo operation error.function.noop.title=Function had no effect error.rearrange.noop=Rearranging points had no effect error.function.notavailable.title=Function not available -error.function.nojava3d=This function requires the Java3d library,\navailable from Sun.com. +error.function.nojava3d=This function requires the Java3d library. error.3d=An error occurred with the 3d display error.readme.notfound=Readme file not found error.osmimage.dialogtitle=Error loading map images @@ -738,3 +814,4 @@ error.cache.notthere=The tile cache directory was not found error.cache.empty=The tile cache directory is empty error.cache.cannotdelete=No tiles could be deleted error.interpolate.invalidparameter=The number of points must be between 1 and 1000 +error.learnestimationparams.failed=Cannot learn the parameters from this track.\nTry loading more tracks. diff --git a/tim/prune/lang/prune-texts_en_US.properties b/tim/prune/lang/prune-texts_en_US.properties index b2b2fc2..928d027 100644 --- a/tim/prune/lang/prune-texts_en_US.properties +++ b/tim/prune/lang/prune-texts_en_US.properties @@ -12,6 +12,7 @@ dialog.setcolours.intro=Click on a color patch to change the color # Measurement units units.metres=Meters units.kilometres=Kilometers +units.metrespersec=meters per second # External urls url.googlemaps=maps.google.com diff --git a/tim/prune/lang/prune-texts_es.properties b/tim/prune/lang/prune-texts_es.properties index 77f8893..2d2f3a5 100644 --- a/tim/prune/lang/prune-texts_es.properties +++ b/tim/prune/lang/prune-texts_es.properties @@ -97,7 +97,6 @@ function.show3d=Mostrar en 3-D function.distances=Distancias function.fullrangedetails=Detalles adicionales de rango function.setmapbg=Configurar fondo de mapa -function.setkmzimagesize=Configurar tama\u00f1os de las im\u00e1genes KMZ function.setpaths=Configurar rutas del programas function.getgpsies=Bajar ruta de Gpsies function.uploadgpsies=Subir recorrido a Gpsies @@ -188,6 +187,7 @@ dialog.exportkml.text=Descripci\u00f3n para los datos dialog.exportkml.altitude=Absoluta altitudes (para aviaci\u00f3n) dialog.exportkml.kmz=Comprimir al archivo kmz dialog.exportkml.exportimages=Exportar fotos al kmz +dialog.exportkml.imagesize=Tama\u00f1os de las im\u00e1genes dialog.exportkml.trackcolour=Color del track dialog.exportgpx.name=Nombre dialog.exportgpx.desc=Descripci\u00f3n @@ -204,7 +204,7 @@ dialog.exportpov.cameraz=C\u00e1mara Z dialog.exportpov.modelstyle=Estilo dialog.exportpov.ballsandsticks=Balas en palos dialog.exportpov.tubesandwalls=Tubos y paredes -dialog.exportpov.warningtracksize=Este track contiene un gran numero de puntos. Puede ser que Java3D no los pueda visualizar. Est\u00e1 seguro de que desea continuar? +dialog.3d.warningtracksize=Este track contiene un gran numero de puntos. Puede ser que Java3D no los pueda visualizar. Est\u00e1 seguro de que desea continuar? dialog.exportsvg.text=Seleccione los par\u00e1metros para exportar a SVG dialog.exportsvg.phi=\u00c1ngulo de azimuth \u03d5 dialog.exportsvg.theta=\u00c1ngulo de elevaci\u00f3n @@ -465,9 +465,6 @@ dialog.searchwikipedianames.search=Buscar: # 3d window dialog.3d.title=GpsPrune vista 3-D dialog.3d.altitudefactor=Factor de exageraci\u00f3n de altura -dialog.3dlines.title=Cuadr\u00edcula GpsPrune -dialog.3dlines.empty=¡No hay ninguna cuadr\u00edcula! -dialog.3dlines.intro=Informaci\u00f3n de la cuadr\u00edcula # Confirm messages confirm.loadfile=Dato cargado de @@ -515,7 +512,6 @@ button.cancel=Cancelar button.overwrite=Sobreescribir button.moveup=Mover hacia arriba button.movedown=Mover hacia abajo -button.showlines=Mostrar cuadr\u00edcula button.edit=Editar button.exit=Salir button.close=Cerrar @@ -701,7 +697,7 @@ error.undofailed.text=No ha sido posible deshacer la operaci\u00f3n error.function.noop.title=La funci\u00f3n no se ha efectuado error.rearrange.noop=Reordenaci\u00f3n de puntos no se ha efectuado error.function.notavailable.title=Funci\u00f3n no disponible -error.function.nojava3d=Esta funci\u00f3n requiere la librer\u00eda Java3d,\ndisponible en Sun.com. +error.function.nojava3d=Esta funci\u00f3n requiere la librer\u00eda Java3d. error.3d=Ha ocurrido un error con la funci\u00f3n 3-D error.readme.notfound=Archivo readme no encontrado error.osmimage.dialogtitle=Error al cargar el mapa diff --git a/tim/prune/lang/prune-texts_fr.properties b/tim/prune/lang/prune-texts_fr.properties index ab11b33..83045ea 100644 --- a/tim/prune/lang/prune-texts_fr.properties +++ b/tim/prune/lang/prune-texts_fr.properties @@ -100,7 +100,6 @@ function.show3d=Montrer en 3D function.distances=Distances function.fullrangedetails=Montrer tous les d\u00e9tails function.setmapbg=D\u00e9finir le fond de carte -function.setkmzimagesize=D\u00e9finir la taille de l'image KMZ function.setpaths=D\u00e9finir les chemins des programmes function.getgpsies=R\u00e9cup\u00e9rer les traces Gpsies function.uploadgpsies=T\u00e9l\u00e9charger la trace sur Gpsies @@ -192,6 +191,7 @@ dialog.exportkml.text=Titre pour les donn\u00e9es dialog.exportkml.altitude=Altitudes absolues (pour l'aviation) dialog.exportkml.kmz=Compresser au format kmz dialog.exportkml.exportimages=Exporter les vignettes au format kmz +dialog.exportkml.imagesize=Taille des images dialog.exportkml.trackcolour=Couleur de la trace dialog.exportgpx.name=Nom dialog.exportgpx.desc=L\u00e9gende @@ -208,7 +208,14 @@ dialog.exportpov.cameraz=Cam\u00e9ra Z dialog.exportpov.modelstyle=Style du mod\u00e8le dialog.exportpov.ballsandsticks=Points et b\u00e2tons dialog.exportpov.tubesandwalls=Tubes et murs -dialog.exportpov.warningtracksize=Cette trace poss\u00e8de un grand nombre de points, Java3D peut ne pas pouvoir l'afficher.\n\u00cates-vous s\u00fbr de vouloir continuer ? +dialog.3d.warningtracksize=Cette trace poss\u00e8de un grand nombre de points, Java3D peut ne pas pouvoir l'afficher.\n\u00cates-vous s\u00fbr de vouloir continuer ? +dialog.baseimage.title=Image de la carte +dialog.baseimage.useimage=Utiliser image +dialog.baseimage.mapsource=Source de cartes +dialog.baseimage.zoom=Zoom +dialog.baseimage.incomplete=Image incompl\u00e8te +dialog.baseimage.tiles=Tuiles +dialog.baseimage.size=Taille de l'image dialog.exportsvg.text=S\u00e9lectionner les param\u00e8tres de l'export SVG dialog.exportsvg.phi=Angle d'azimuth \u03d5 dialog.exportsvg.theta=Angle d'\u00e9l\u00e9vation \u03b8 @@ -470,6 +477,7 @@ dialog.diskcache.deleteall=Efface toute les tuiles dialog.diskcache.deleted1=Effac\u00e9 dialog.diskcache.deleted2=tuiles du cache dialog.deletefieldvalues.intro=Choisir le champ \u00e0 effacer pour l'\u00e9tendue actuelle +dialog.deletefieldvalues.nofields=L'\u00e9tendue actuelle n'a pas de champs \u00e0 effacer dialog.setlinewidth.text=Entrer l'\u00e9paisseur des lignes des traces (1-4) dialog.downloadosm.desc=Confirmer le t\u00e9l\u00e9chargement des donn\u00e9es OSM brutes pour la zone indiqu\u00e9e : dialog.searchwikipedianames.search=Chercher : @@ -477,9 +485,6 @@ dialog.searchwikipedianames.search=Chercher : # 3d window dialog.3d.title=Vue 3D de GpsPrune dialog.3d.altitudefactor=Facteur d'exag\u00e9ration de l'altitude -dialog.3dlines.title=Grille de GpsPrune -dialog.3dlines.empty=Pas de grille \u00e0 afficher ! -dialog.3dlines.intro=Ceci est la grille pour la vue 3D # Confirm messages confirm.loadfile=Donn\u00e9es charg\u00e9es depuis le fichier @@ -528,7 +533,6 @@ button.cancel=Annuler button.overwrite=\u00c9craser button.moveup=Monter button.movedown=Descendre -button.showlines=Montrer les lignes button.edit=\u00c9diter button.exit=Terminer button.close=Fermer @@ -720,7 +724,7 @@ error.undofailed.text=\u00c9chec de l'op\u00e9ration d'annulation error.function.noop.title=Fonction sans effet error.rearrange.noop=R\u00e9arrangement des points sans effet error.function.notavailable.title=Function non-disponible -error.function.nojava3d=Cette fonction n\u00e9cessite la librairie Java3d,\ndisponible sur Sun.com. +error.function.nojava3d=Cette fonction n\u00e9cessite la librairie Java3d. error.3d=Un probl\u00e8me est survenu avec l'affichage 3D error.readme.notfound=Fichier Lisez-moi introuvable error.osmimage.dialogtitle=Erreur au chargement des portions de cartes diff --git a/tim/prune/lang/prune-texts_hu.properties b/tim/prune/lang/prune-texts_hu.properties index 837f9d9..0764c74 100644 --- a/tim/prune/lang/prune-texts_hu.properties +++ b/tim/prune/lang/prune-texts_hu.properties @@ -97,7 +97,6 @@ function.show3d=3D n\u00e9zet function.distances=T\u00e1vols\u00e1gok function.fullrangedetails=Teljes tartom\u00e1ny r\u00e9szletei function.setmapbg=H\u00e1tt\u00e9rk\u00e9p be\u00e1ll\u00edt\u00e1sa -function.setkmzimagesize=KMZ k\u00e9pm\u00e9ret be\u00e1ll\u00edt\u00e1sa function.setpaths=Program\u00fatvonalak be\u00e1ll\u00edt\u00e1sa function.getgpsies=Gpsies nyomvonalak let\u00f6lt\u00e9se function.uploadgpsies=Nyomvonal felt\u00f6lt\u00e9se Gpsiesra @@ -204,7 +203,7 @@ dialog.exportpov.cameraz=Z kamera dialog.exportpov.modelstyle=Modell st\u00edlusa dialog.exportpov.ballsandsticks=Goly\u00f3k \u00e9s botok dialog.exportpov.tubesandwalls=Cs\u00f6vek \u00e9s falak -dialog.exportpov.warningtracksize=Ez a nyomvonal nagy sz\u00e1m\u00fa pontot tartalmaz, amelyet a Java3D nem biztos, hogy meg tud jelen\u00edteni.\nBiztos benne, hogy folytatni szeretn\u00e9? +dialog.3d.warningtracksize=Ez a nyomvonal nagy sz\u00e1m\u00fa pontot tartalmaz, amelyet a Java3D nem biztos, hogy meg tud jelen\u00edteni.\nBiztos benne, hogy folytatni szeretn\u00e9? dialog.exportsvg.text=Param\u00e9terek kiv\u00e1laszt\u00e1sa az SVG exporthoz dialog.exportsvg.phi=Ir\u00e1nysz\u00f6g \u03d5 dialog.exportsvg.theta=Emel\u00e9s sz\u00f6ge \u03b8 @@ -465,9 +464,6 @@ dialog.searchwikipedianames.search=Keres\u00e9s erre: # 3d window dialog.3d.title=GpsPrune 3D n\u00e9zet dialog.3d.altitudefactor=Magass\u00e1gi ny\u00fajt\u00e1si t\u00e9nyez\u0151 -dialog.3dlines.title=GpsPrune r\u00e1csvonalak -dialog.3dlines.empty=Nincsenek megjelen\u00edthet\u0151 r\u00e1csvonalak! -dialog.3dlines.intro=Ezek a r\u00e1csvonalak a 3D n\u00e9zethez # Confirm messages confirm.loadfile=Adatok f\u00e1jlb\u00f3l bet\u00f6ltve @@ -515,7 +511,6 @@ button.cancel=M\u00e9gse button.overwrite=Fel\u00fcl\u00edr\u00e1s button.moveup=Mozgat\u00e1s feljebb button.movedown=Mozgat\u00e1s lejjebb -button.showlines=Sorok megjelen\u00edt\u00e9se button.edit=Szerkeszt\u00e9s button.exit=Kil\u00e9p\u00e9s button.close=Bez\u00e1r\u00e1s @@ -701,7 +696,7 @@ error.undofailed.text=A m\u0171velet visszavon\u00e1sa nem siker\u00fclt error.function.noop.title=A funkci\u00f3 nem eredm\u00e9nyezett v\u00e1ltoz\u00e1st error.rearrange.noop=A pontok \u00fajrarendez\u00e9se nem eredm\u00e9nyezett v\u00e1ltoz\u00e1st error.function.notavailable.title=A funkci\u00f3 nem \u00e9rhet\u0151 el -error.function.nojava3d=Ehhez a funkci\u00f3hoz a Java3d f\u00fcggv\u00e9nyk\u00f6nyvt\u00e1r sz\u00fcks\u00e9ges,\n amely a Sun.com webhelyr\u0151l \u00e9rhet\u0151 el. +error.function.nojava3d=Ehhez a funkci\u00f3hoz a Java3d f\u00fcggv\u00e9nyk\u00f6nyvt\u00e1r sz\u00fcks\u00e9ges. error.3d=Hiba t\u00f6rt\u00e9nt a 3d megjelen\u00edt\u00e9ssel error.readme.notfound=Az olvassel f\u00e1jl nem tal\u00e1lhat\u00f3 error.osmimage.dialogtitle=Hiba a t\u00e9rk\u00e9p bet\u00f6lt\u00e9sekor diff --git a/tim/prune/lang/prune-texts_it.properties b/tim/prune/lang/prune-texts_it.properties index 989110a..07027a0 100644 --- a/tim/prune/lang/prune-texts_it.properties +++ b/tim/prune/lang/prune-texts_it.properties @@ -100,7 +100,6 @@ function.show3d=Mostra in 3D function.distances=Mostra distanze function.fullrangedetails=Mostra dettagli function.setmapbg=Configura sfondo mappa -function.setkmzimagesize=Configura dimensione immagine KMZ function.setpaths=Configura percorsi programmi function.getgpsies=Ottieni traccie da Gpsies function.uploadgpsies=Carica traccia su Gpsies @@ -208,7 +207,8 @@ dialog.exportpov.cameraz=Camera Z dialog.exportpov.modelstyle=Stile del modello dialog.exportpov.ballsandsticks=Palle e bacchette dialog.exportpov.tubesandwalls=Tubi e pareti -dialog.exportpov.warningtracksize=Questa traccia ha un elevato numero di punti, e Java3D potrebbe non essere in grado di visualizzarli.\nSei sicuro di voler continuare? +dialog.3d.warningtracksize=Questa traccia ha un elevato numero di punti, e Java3D potrebbe non essere in grado di visualizzarli.\nSei sicuro di voler continuare? +dialog.exportkml.imagesize=Dimensione immagine dialog.exportsvg.text=Seleziona i parametri per esportare in SVG dialog.exportsvg.phi=Angolo orizzontale \u03d5 dialog.exportsvg.theta=Angolo di elevazione \u03b8 @@ -470,6 +470,7 @@ dialog.diskcache.deleteall=Cancellare tutti tasselli dialog.diskcache.deleted1=Cancellati dialog.diskcache.deleted2=files dal cache dialog.deletefieldvalues.intro=Selezione il campo da cancellare dall'intervallo corrente +dialog.deletefieldvalues.nofields=Nell'intervallo selezionato non ci sono campi da cancellare dialog.setlinewidth.text=Specifica il tratteggio delle linee per disegnare la traccia (1-4) dialog.downloadosm.desc=Conferma lo scarico dei dati raw OSM per l'area specificata: dialog.searchwikipedianames.search=Cerca per: @@ -477,9 +478,6 @@ dialog.searchwikipedianames.search=Cerca per: # 3d window dialog.3d.title=Visione GpsPrune in 3D dialog.3d.altitudefactor=Fattore di moltiplicazione della quota -dialog.3dlines.title=Griglia di GpsPrune -dialog.3dlines.empty=Nessuna griglia mostrata! -dialog.3dlines.intro=Queste sono le linee della griglia per la visione 3D # Confirm messages confirm.loadfile=Dati caricati da file @@ -528,7 +526,6 @@ button.cancel=Annulla button.overwrite=Sovrascrivi button.moveup=Sposta in alto button.movedown=Sposta in basso -button.showlines=Mostra linee button.edit=Modifica button.exit=Esci button.close=Chiudi @@ -672,7 +669,6 @@ undo.removephoto=rimuovi foto undo.removeaudio=rimuovi riprese audio undo.deleterange=cancella l'intervallo undo.croptrack=taglia la traccia -undo.deletemarked= undo.insert=inserisci punti undo.reverse=inverti l'intervallo undo.mergetracksegments=unisci segmenti traccia @@ -720,7 +716,7 @@ error.undofailed.text=Impossibile annullare l'operazione error.function.noop.title=La funzione non ha avuto effetto error.rearrange.noop=La riorganizzazione dei punto non ha avuto effetto error.function.notavailable.title=Funzione non disponibile -error.function.nojava3d=Questa funzione richiede la libreria Java3d,\ndisponibile all'indirizzo Sun.com. +error.function.nojava3d=Questa funzione richiede la libreria Java3d. error.3d=\u00c8 avvenuto un errore nella visualizzazione 3D error.readme.notfound=Non ho trovato il file Leggimi error.osmimage.dialogtitle=Errore nel caricamento nelle immagini della mappa diff --git a/tim/prune/lang/prune-texts_ja.properties b/tim/prune/lang/prune-texts_ja.properties index 33b54c9..9739f87 100644 --- a/tim/prune/lang/prune-texts_ja.properties +++ b/tim/prune/lang/prune-texts_ja.properties @@ -54,6 +54,26 @@ menu.map.autopan=\u81ea\u52d5\u79fb\u52d5 menu.map.showmap=\u5730\u56f3\u3092\u8868\u793a menu.map.showscalebar=\u7e2e\u5c3a\u8868\u793a +# Alt keys for menus +altkey.menu.file=F +altkey.menu.track=T +altkey.menu.range=R +altkey.menu.point=P +altkey.menu.view=V +altkey.menu.photo=O +altkey.menu.audio=A +altkey.menu.settings=S +altkey.menu.help=H + +# Ctrl shortcuts for menu items +shortcut.menu.file.open=O +shortcut.menu.file.load=L +shortcut.menu.file.save=S +shortcut.menu.track.undo=Z +shortcut.menu.edit.compress=C +shortcut.menu.range.all=A +shortcut.menu.help.help=H + # Functions function.open=\u30d5\u30a1\u30a4\u30eb\u3092\u958b\u304f function.importwithgpsbabel=GPSBabel\u306e\u30d5\u30a1\u30a4\u30eb\u3092\u30a4\u30f3\u30dd\u30fc\u30c8 @@ -63,6 +83,7 @@ function.exportkml=KML\u306b\u30a8\u30af\u30b9\u30dd\u30fc\u30c8 function.exportgpx=GPX\u306b\u30a8\u30af\u30b9\u30dd\u30fc\u30c8 function.exportpov=POV\u306b\u30a8\u30af\u30b9\u30dd\u30fc\u30c8 function.exportsvg=SVG\u306b\u30a8\u30af\u30b9\u30dd\u30fc\u30c8 +function.exportimage=\u30a4\u30e1\u30fc\u30b8\u3092\u30a8\u30af\u30b9\u30dd\u30fc\u30c8 function.editwaypointname=\u30a6\u30a7\u30a4\u30dd\u30a4\u30f3\u30c8\u306e\u540d\u524d\u3092\u7de8\u96c6 function.compress=\u30c8\u30e9\u30c3\u30af\u3092\u5727\u7e2e function.deleterange=\u7bc4\u56f2\u3092\u524a\u9664 @@ -78,7 +99,6 @@ function.show3d=3-D\u30d3\u30e5\u30fc function.distances=\u8ddd\u96e2 function.fullrangedetails=\u5168\u7bc4\u56f2\u8a73\u7d30 function.setmapbg=\u80cc\u666f\u5730\u56f3 -function.setkmzimagesize=KML \u30a4\u30e1\u30fc\u30b8\u30b5\u30a4\u30ba function.setpaths=\u5916\u90e8\u30d7\u30ed\u30b0\u30e9\u30e0\u30d1\u30b9\u3092\u8a2d\u5b9a function.getgpsies=Gpsies\u30c8\u30e9\u30c3\u30af\u3092\u5f97\u308b function.uploadgpsies=Gpsies\u306b\u30c8\u30e9\u30c3\u30af\u3092\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9 @@ -170,6 +190,7 @@ dialog.exportkml.altitude=\u7d76\u5bfe\u9ad8\u5ea6\uff08\u822a\u7a7a\u7528\uff09 dialog.exportkml.kmz=KMZ\u30d5\u30a1\u30a4\u30eb\u3092\u5727\u7e2e dialog.exportkml.exportimages=KMZ\u3078\u753b\u50cf\u306e\u7e2e\u5c0f\u7248\u3092\u4fdd\u5b58 dialog.exportkml.trackcolour=\u30c8\u30e9\u30c3\u30af\u306e\u8272 +dialog.exportkml.standardkml=\u6a19\u6e96 KML dialog.exportgpx.name=\u540d\u524d dialog.exportgpx.desc=\u8a18\u8ff0 dialog.exportgpx.includetimestamps=\u6642\u9593\u8a18\u9332\u3092\u542b\u3080 @@ -185,7 +206,7 @@ dialog.exportpov.cameraz=\u30ab\u30e1\u30e9 Z dialog.exportpov.modelstyle=\u30e2\u30c7\u30eb\u30b9\u30bf\u30a4\u30eb dialog.exportpov.ballsandsticks=\u7403\u3068\u68d2 dialog.exportpov.tubesandwalls=\u7ba1\u3068\u58c1 -dialog.exportpov.warningtracksize=\u3053\u306e\u30c8\u30e9\u30c3\u30af\u306f\u975e\u5e38\u306b\u591a\u304f\u306e\u70b9\u304c\u3042\u308b\u306e\u3067\u3001Java3D \u3067\u306f\u8868\u793a\u3057\u5207\u308c\u306a\u3044\u3068\u601d\u308f\u308c\u307e\u3059\u3002\n\u7d9a\u3051\u307e\u3059\u304b\uff1f +dialog.3d.warningtracksize=\u3053\u306e\u30c8\u30e9\u30c3\u30af\u306f\u975e\u5e38\u306b\u591a\u304f\u306e\u70b9\u304c\u3042\u308b\u306e\u3067\u3001Java3D \u3067\u306f\u8868\u793a\u3057\u5207\u308c\u306a\u3044\u3068\u601d\u308f\u308c\u307e\u3059\u3002\n\u7d9a\u3051\u307e\u3059\u304b\uff1f dialog.pointtype.desc=\u6b21\u306e\u70b9\u3092\u4fdd\u5b58\u3057\u307e\u3059\uff1a dialog.pointtype.track=\u30c8\u30e9\u30c3\u30af\u30dd\u30a4\u30f3\u30c8 dialog.pointtype.waypoint=\u30a6\u30a7\u30a4\u30dd\u30a4\u30f3\u30c8 @@ -407,9 +428,6 @@ dialog.searchwikipedianames.search=\u53f3\u8a18\u3092\u691c\u7d22: # 3d window dialog.3d.title=GpsPrune 3D \u8868\u793a -dialog.3dlines.title=GpsPrune \u683c\u5b50\u7dda -dialog.3dlines.empty=\u683c\u5b50\u7dda\u304c\u8868\u793a\u3055\u308c\u307e\u305b\u3093 -dialog.3dlines.intro=\u3053\u308c\u3089\u304c 3D \u8868\u793a\u7528\u306e\u683c\u5b50\u7dda\u3067\u3059\u3002 # Confirm messages confirm.loadfile=\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u30c7\u30fc\u30bf\u3092\u8aad\u307f\u8fbc\u3080 @@ -457,7 +475,6 @@ button.cancel=\u53d6\u6d88 button.overwrite=\u4e0a\u66f8\u304d button.moveup=\u4e0a\u3078 button.movedown=\u4e0b\u3078 -button.showlines=\u7dda\u3092\u8868\u793a button.edit=\u7de8\u96c6 button.exit=\u7d42\u4e86 button.close=\u9589\u3058\u308b @@ -489,6 +506,7 @@ filetype.kmz=KMZ\u30d5\u30a1\u30a4\u30eb filetype.gpx=GPX\u30d5\u30a1\u30a4\u30eb filetype.pov=POV\u30d5\u30a1\u30a4\u30eb filetype.svg=SVG\u30d5\u30a1\u30a4\u30eb +filetype.png=PNG\u30d5\u30a1\u30a4\u30eb filetype.audio=MP3,OGG,WAV\u30d5\u30a1\u30a4\u30eb # Display components @@ -569,6 +587,7 @@ units.milesperhour.short=mph units.metrespersec.short=m/s units.feetpersec.short=ft/s units.hours=\u6642\u9593 +units.seconds=\u79d2 units.degminsec=\u5ea6-\u5206-\u79d2 units.degmin=\u5ea6-\u5206 units.deg=\u5ea6 diff --git a/tim/prune/lang/prune-texts_ko.properties b/tim/prune/lang/prune-texts_ko.properties index 68c5cd7..b2b4596 100644 --- a/tim/prune/lang/prune-texts_ko.properties +++ b/tim/prune/lang/prune-texts_ko.properties @@ -94,7 +94,6 @@ function.show3d=3\ucc28\uc6d0 \ubcf4\uae30 function.distances=\uac70\ub9ac function.fullrangedetails=\uc5f0\uacb0\uc120 \uc0c1\uc138 \uc815\ubcf4 \ubcf4\uae30 function.setmapbg=\ubc30\uacbd \uc9c0\ub3c4 \uc9c0\uc815 -function.setkmzimagesize=KMZ \uadf8\ub9bc \ud06c\uae30 \uc9c0\uc815 function.setpaths=\uc678\ubd80\ud504\ub85c\uadf8\ub7a8 \uc9c0\uc815 function.getgpsies=gpsies\uc5d0\uc11c \ud2b8\ub799\ubaa9\ub85d \uc5bb\uae30 function.uploadgpsies=gpsies\ub85c \ud2b8\ub799 \uc62c\ub9ac\uae30 @@ -197,7 +196,7 @@ dialog.exportpov.cameraz=\uce74\uba54\ub77c\uc758 Z \uc88c\ud45c dialog.exportpov.modelstyle=\ubaa8\ub378 \uc2a4\ud0c0\uc77c dialog.exportpov.ballsandsticks=\ub9c9\ub300\uae30\uc640 \uacf5 dialog.exportpov.tubesandwalls=\ubcbd\uacfc \ud29c\ube0c -dialog.exportpov.warningtracksize=\uc774 \ud2b8\ub799\uc740 \uc9c0\uc810\uc774 \ub108\ubb34 \ub9ce\uc544 Java3D\uac00 \ud45c\ud604 \ubabb\ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4. /n \uadf8\ub798\ub3c4 \uacc4\uc18d \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? +dialog.3d.warningtracksize=\uc774 \ud2b8\ub799\uc740 \uc9c0\uc810\uc774 \ub108\ubb34 \ub9ce\uc544 Java3D\uac00 \ud45c\ud604 \ubabb\ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4. /n \uadf8\ub798\ub3c4 \uacc4\uc18d \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? dialog.exportsvg.text=SVG\ub85c \ub0b4\ubcf4\ub0bc \ud30c\ub77c\ubbf8\ud130\ub97c \uc120\ud0dd\ud558\uc138\uc694 dialog.exportsvg.phi=\ubc29\uc704\uac01(\u03d5) dialog.exportsvg.theta=\uace0\ub3c4(\u03b8) @@ -442,9 +441,6 @@ dialog.searchwikipedianames.search=\ucc3e\uae30 # 3d window dialog.3d.title=GpsPrune 3D \ubcf4\uae30 dialog.3d.altitudefactor=\uace0\ub3c4 \uacfc\uc7a5 \uacc4\uc218 -dialog.3dlines.title=GpsPrune \uaca9\uc790\uc120 -dialog.3dlines.empty=\uaca9\uc790\uc120 \uc5c6\uc774 \ud45c\uc2dc\ud558\uae30 -dialog.3dlines.intro=3D \ubcf4\uae30\ub97c \uc704\ud55c \uaca9\uc790\uc120\uc785\ub2c8\ub2e4. # Confirm messages confirm.loadfile=\ud30c\uc77c\uc5d0\uc11c \uc790\ub8cc\ub97c \ubd88\ub7ec\uc654\uc5b4\uc694. @@ -492,7 +488,6 @@ button.cancel=\ucde8\uc18c button.overwrite=\ub36e\uc5b4\uc4f0\uae30 button.moveup=\uc704\ub85c button.movedown=\uc544\ub798\ub85c -button.showlines=\uc120 \ubcf4\uae30 button.edit=\uc218\uc815 button.exit=\ub098\uac00\uae30 button.close=\ub2eb\uae30 diff --git a/tim/prune/lang/prune-texts_nl.properties b/tim/prune/lang/prune-texts_nl.properties index 5a75c2a..12c9307 100644 --- a/tim/prune/lang/prune-texts_nl.properties +++ b/tim/prune/lang/prune-texts_nl.properties @@ -84,6 +84,7 @@ function.exportkml=Export KML function.exportgpx=Export GPX function.exportpov=Export POV function.exportsvg=Export SVG +function.exportimage=Bestand exporteren function.editwaypointname=Hernoem waypoint function.compress=Route comprimeren function.deleterange=Verwijder reeks @@ -99,8 +100,9 @@ function.charts=Diagram function.show3d=3D beeld function.distances=Afstanden function.fullrangedetails=Reeks details +function.estimatetime=Geschatte tijd +function.learnestimationparams=Parameters voor geschatte tijd function.setmapbg=Instellen kaart achtergrond -function.setkmzimagesize=Instellen KMZ afbeelding grootte function.setpaths=Instellen programmapaden function.getgpsies=Routes van Gpsies ophalen function.uploadgpsies=Upload routes naar Gpsies @@ -159,6 +161,10 @@ dialog.openoptions.deliminfo.records=bestanden, met dialog.openoptions.deliminfo.fields=velden dialog.openoptions.deliminfo.norecords=Geen bestanden dialog.openoptions.altitudeunits=Hoogte eenheden +dialog.openoptions.speedunits=Snelheid eenheden +dialog.openoptions.vertspeedunits=Verticale snelheid eenheden +dialog.openoptions.vspeed.positiveup=Positieve snelheid omhoog +dialog.openoptions.vspeed.positivedown=Positieve snelheid omlaag dialog.open.contentsdoubled=Dit bestand bevat twee kopie\u00ebn van ieder punt,\neen keer als waypoint en een keer als punt dialog.selecttracks.intro=Selecteer route of routes om te laden dialog.selecttracks.noname=Onbenoemd @@ -176,6 +182,30 @@ dialog.gpsload.save=Opslaan naar bestand dialog.gpssend.sendwaypoints=Verstuur waypoint dialog.gpssend.sendtracks=Verstuur routes dialog.gpssend.trackname=Routenaam +dialog.gpsbabel.filters=Filters +dialog.addfilter.title=Filter toevoegen +dialog.gpsbabel.filter.discard=Verwijderen +dialog.gpsbabel.filter.simplify=Vereenvoudigen +dialog.gpsbabel.filter.distance=Afstand +dialog.gpsbabel.filter.interpolate=Interpoleren +dialog.gpsbabel.filter.discard.intro=Verwijder punten indien +dialog.gpsbabel.filter.discard.hdop=Hdop > +dialog.gpsbabel.filter.discard.vdop=Vdop < +dialog.gpsbabel.filter.discard.numsats=Aantal satellieten < +dialog.gpsbabel.filter.discard.nofix=Punt heeft geen gps fix +dialog.gpsbabel.filter.discard.unknownfix=Punt heeft onbekende gps fix +dialog.gpsbabel.filter.simplify.intro=Verwijder punten tot +dialog.gpsbabel.filter.simplify.maxpoints=Aantal punten < +dialog.gpsbabel.filter.simplify.maxerror=of fout-afstand < +dialog.gpsbabel.filter.simplify.crosstrack=cross-track +dialog.gpsbabel.filter.simplify.length=lengteverschil +dialog.gpsbabel.filter.simplify.relative=relatief aan hdop +dialog.gpsbabel.filter.distance.intro=Verwijder punten indien nabij enig eerder punt +dialog.gpsbabel.filter.distance.distance=Indien afstand < +dialog.gpsbabel.filter.distance.time=en tijdsverschil < +dialog.gpsbabel.filter.interpolate.intro=Voeg extra punten toe tussen route punten +dialog.gpsbabel.filter.interpolate.distance=Indien afstand > +dialog.gpsbabel.filter.interpolate.time=of tijdsverschil > dialog.saveoptions.title=Bestand opslaan dialog.save.fieldstosave=Velden op te slaan dialog.save.table.field=Veld @@ -192,7 +222,10 @@ dialog.exportkml.text=Titel voor de data dialog.exportkml.altitude=Absolute hoogten (voor luchtvaart) dialog.exportkml.kmz=Comprimeren voor kmz bestand dialog.exportkml.exportimages=Exporteer thumbnails naar kzm +dialog.exportkml.imagesize=Afbeeldinggrootte dialog.exportkml.trackcolour=Routekleur +dialog.exportkml.standardkml=Standaard KML +dialog.exportkml.extendedkml=Uitgebreide KML met tijden dialog.exportgpx.name=Naam dialog.exportgpx.desc=Omschrijving dialog.exportgpx.includetimestamps=Tijden meenemen @@ -208,11 +241,23 @@ dialog.exportpov.cameraz=Camera Z dialog.exportpov.modelstyle=Model stijl dialog.exportpov.ballsandsticks=Balletjes en stokjes dialog.exportpov.tubesandwalls=Buizen en muren -dialog.exportpov.warningtracksize=Deze route heeft een groot aantal punten. Java3D kan deze mogelijk niet tonen.\nWeet u zeker dat u door wilt gaan? +dialog.3d.warningtracksize=Deze route heeft een groot aantal punten. Java3D kan deze mogelijk niet tonen.\nWeet u zeker dat u door wilt gaan? +dialog.exportpov.baseimage=Basisafbeelding +dialog.exportpov.cannotmakebaseimage=Kan basisafbeelding niet opslaan +dialog.baseimage.title=Basisafbeelding +dialog.baseimage.useimage=Gebruik afbeelding +dialog.baseimage.mapsource=Kaartbron +dialog.baseimage.zoom=Zoom niveau +dialog.baseimage.incomplete=Afbeelding onvolledig +dialog.baseimage.tiles=Tegels +dialog.baseimage.size=Afbeeldinggrootte dialog.exportsvg.text=Selecteer de camera hoeken voor SVG export dialog.exportsvg.phi=Azimut hoek \u03d5 dialog.exportsvg.theta=Stijgingshoek \u03b8 dialog.exportsvg.gradients=Gebruik gradaties voor schaduw +dialog.exportimage.noimagepossible=Kaartafbeeldingen dienen gecached te worden naar disk om ze te kunnen exporteren. +dialog.exportimage.drawtrack=Teken route op kaart +dialog.exportimage.textscalepercent=Tekstschaal factor (%) dialog.pointtype.desc=Sla de volgende punttypen op: dialog.pointtype.track=Routepunten dialog.pointtype.waypoint=Waypoints @@ -233,7 +278,9 @@ dialog.clearundo.title=Ongedaan-maken lijst wissen dialog.clearundo.text=Weet u zeker dat u de ongedaan-maken lijst wilt wissen?\nAlle informatie zal verloren gaan! dialog.pointedit.title=Wijzig punt dialog.pointedit.text=Selecteer ieder te wijzigen veld en gebruik de "Wijzigen" knop om de waarden aan te passen. +dialog.pointedit.intro=Selecteer telkens een veld om de waarde te zien en te wijzigen dialog.pointedit.table.field=Veld +dialog.pointedit.nofield=Geen veld geselecteerd dialog.pointedit.table.value=Waarde dialog.pointedit.table.changed=Gewijzigd dialog.pointedit.changevalue.text=Geef de nieuwe waarde voor dit veld @@ -279,6 +326,27 @@ dialog.distances.toofewpoints=Deze functie heeft waypoints nodig om de afstand e dialog.fullrangedetails.intro=Dit zijn de details van de geselecteerde reeks dialog.fullrangedetails.coltotal=Inclusief hiaten dialog.fullrangedetails.colsegments=Zonder hiaten +dialog.estimatetime.details=Details +dialog.estimatetime.gentle=Licht +dialog.estimatetime.steep=Steil +dialog.estimatetime.climb=Klimmen +dialog.estimatetime.descent=Afdaling +dialog.estimatetime.parameters=Parameters +dialog.estimatetime.parameters.timefor=Tijd voor +dialog.estimatetime.results=Resultaten +dialog.estimatetime.results.estimatedtime=Geschatte tijd +dialog.estimatetime.results.actualtime=Daadwerkelijke tijd +dialog.estimatetime.error.nodistance=Tijdschattingen hebben geconnecteerde routepunten nodig om een afstand te bepalen +dialog.estimatetime.error.noaltitudes=Deze selectie bevat geen hoogteinformatie +dialog.learnestimationparams.intro=Dit zijn de parameters berekend voor deze route +dialog.learnestimationparams.averageerror=Gemiddelde fout +dialog.learnestimationparams.combine=Deze parameters kunnen gecombineerd worden met de huidige waarden +dialog.learnestimationparams.combinedresults=Gecombineerde resultaten +dialog.learnestimationparams.weight.100pccurrent=Behoud huidige waarden +dialog.learnestimationparams.weight.current=huidig +dialog.learnestimationparams.weight.calculated=berekend +dialog.learnestimationparams.weight.50pc=Gemiddelde van huidige en berekende waarden +dialog.learnestimationparams.weight.100pccalculated=Gebruik nieuwe berekende waarden dialog.setmapbg.intro=Selecteer een kaart-bron, of voeg een nieuwe bron toe. dialog.addmapsource.title=Nieuwe kaart-bron toevoegen dialog.addmapsource.sourcename=Naam van de bron @@ -478,9 +546,6 @@ dialog.searchwikipedianames.search=Zoeken naar: # 3d window dialog.3d.title=GpsPrune in 3D dialog.3d.altitudefactor=Hoogte overdrijvingsfactor -dialog.3dlines.title=GpsPrune raster -dialog.3dlines.empty=Geen raster om af te beelden -dialog.3dlines.intro=Dit is het raster voor 3D # Confirm messages confirm.loadfile=Data van schijf geladen @@ -529,7 +594,6 @@ button.cancel=Annuleren button.overwrite=Overschrijven button.moveup=Omhoog button.movedown=Omlaag -button.showlines=Toon raster button.edit=Wijzigen button.exit=Afsluiten button.close=Sluiten @@ -552,6 +616,7 @@ button.browse=Browse... button.addnew=Nieuwe toevoegen button.delete=Verwijderen button.manage=Beheer +button.combine=Samenvoegen # File types filetype.txt=TXT bestand @@ -562,12 +627,14 @@ filetype.kmz=KMZ bestand filetype.gpx=GPX bestand filetype.pov=POV bestand filetype.svg=SVG bestand +filetype.png=PNG bestand filetype.audio=MP3, OGG, WAV bestanden # Display components display.nodata=Geen gegevens geladen display.noaltitudes=Route gegevens bevatten geen hoogte display.notimestamps=Route gegevens bevatten geen tijdinformatie +display.novalues=Route gegevens bevatten geen waarden voor dit veld details.trackdetails=Route details details.notrack=Geen route geladen details.track.points=Punten @@ -638,21 +705,31 @@ units.feet=Voet units.feet.short=ft units.kilometres=Kilometers units.kilometres.short=km +units.kilometresperhour=km per uur units.kilometresperhour.short=km/u units.miles=Mijlen units.miles.short=mi +units.milesperhour=mijlen per uur units.milesperhour.short=mph units.nauticalmiles=Nautische mijlen units.nauticalmiles.short=N.m. units.nauticalmilesperhour.short=Kn +units.metrespersec=meters per seconde units.metrespersec.short=m/s +units.feetpersec=feet per seconde units.feetpersec.short=ft/s units.hours=uren +units.minutes=minuten +units.seconds=seconden units.degminsec=Grd-min-sec units.degmin=Grd-min units.deg=Graden units.iso8601=ISO 8601 +# How to combine conditions, such as filters +logic.and=en +logic.or=of + # External urls url.googlemaps=maps.google.nl wikipedia.lang=nl @@ -721,7 +798,7 @@ error.undofailed.text=Kon actie niet terugdraaien error.function.noop.title=Functie had geen effect error.rearrange.noop=Herschikken van punten had geen effect error.function.notavailable.title=Functie niet beschikbaar -error.function.nojava3d=Deze functie heeft Java3d nodig,\nverkrijgbaar bij sun.com. +error.function.nojava3d=Deze functie heeft Java3d nodig. error.3d=Er is een fout opgetreden bij de 3d afbeelding error.readme.notfound=Leesmij bestand niet gevonden error.osmimage.dialogtitle=Fout bij inlezen kaart afbeeldingen @@ -738,3 +815,4 @@ error.cache.notthere=De tegelcache map niet gevonden error.cache.empty=De tegelcache map is leeg error.cache.cannotdelete=Er konden geen tegels verwijderd worden error.interpolate.invalidparameter=Aantal punten moet tussen 1 en 1000 liggen +error.learnestimationparams.failed=Kan geen parameters bepalen van deze route.\nProbeer meer routes te laden. diff --git a/tim/prune/lang/prune-texts_pl.properties b/tim/prune/lang/prune-texts_pl.properties index 2ff8980..7972f59 100644 --- a/tim/prune/lang/prune-texts_pl.properties +++ b/tim/prune/lang/prune-texts_pl.properties @@ -80,10 +80,11 @@ function.open=Otw\u00f3rz function.importwithgpsbabel=Importuj plik z GPSBabel function.loadfromgps=\u0141aduj z GPS function.sendtogps=Wy\u015blij dane do urz\u0105dzenia GPS -function.exportkml=Eksportuj KML +function.exportkml=Eksportuj jako KML function.exportgpx=Eksportuj jako GPX function.exportpov=Eksportuj jako POV function.exportsvg=Eksportuj jako SVG +function.exportimage=Eksportuj jako obraz function.editwaypointname=Zmie\u0144 nazw\u0119 punktu po\u015bredniego function.compress=Kompresuj \u015bcie\u017ck\u0119 function.deleterange=Usu\u0144 zakres @@ -99,8 +100,8 @@ function.charts=Wykres function.show3d=Poka\u017c model 3D function.distances=Odleg\u0142o\u015bci function.fullrangedetails=Wszystkie detale +function.estimatetime=Przewidywany czas function.setmapbg=Wybierz map\u0119 t\u0142a -function.setkmzimagesize=Ustaw rozmiar zdj\u0119\u0107 w KMZ function.setpaths=Ustaw \u015bcie\u017cki do program\u00f3w function.getgpsies=Pobierz \u015bcie\u017cki z Gpsies function.uploadgpsies=Wy\u015blij \u015bcie\u017cki do Gpsies @@ -159,6 +160,8 @@ dialog.openoptions.deliminfo.records=rekordy, z dialog.openoptions.deliminfo.fields=pola dialog.openoptions.deliminfo.norecords=Brak rekord\u00f3w dialog.openoptions.altitudeunits=Jednostki wysoko\u015bci +dialog.openoptions.speedunits=Jednostki pr\u0119dko\u015bci +dialog.openoptions.vertspeedunits=Jednostki pr\u0119dko\u015bci pionowej dialog.open.contentsdoubled=Ten plik zawiera dwie kopie ka\u017cdego punktu.\nRaz jako punkt po\u015bredni, a raz jako punkt \u015bcie\u017cki. dialog.selecttracks.intro=Wybierz \u015bcie\u017ck\u0119 lub \u015bcie\u017cki dialog.selecttracks.noname=Nienazwane @@ -176,6 +179,30 @@ dialog.gpsload.save=Zapisz do pliku dialog.gpssend.sendwaypoints=Wy\u015blij punkty po\u015brednie dialog.gpssend.sendtracks=Wy\u015blij \u015bcie\u017cki dialog.gpssend.trackname=Nazwa \u015bcie\u017cki +dialog.gpsbabel.filters=Filtry +dialog.addfilter.title=Dodaj filtr +dialog.gpsbabel.filter.discard=Odrzu\u0107 +dialog.gpsbabel.filter.simplify=Upro\u015b\u0107 +dialog.gpsbabel.filter.distance=Odleg\u0142o\u015b\u0107 +dialog.gpsbabel.filter.interpolate=Wstaw pomi\u0119dzy +dialog.gpsbabel.filter.discard.intro=Odrzu\u0107 punkty je\u015bli +dialog.gpsbabel.filter.discard.hdop=Hdop > +dialog.gpsbabel.filter.discard.vdop=Vdop > +dialog.gpsbabel.filter.discard.numsats=Ilo\u015b\u0107 satelit\u00f3w < +dialog.gpsbabel.filter.discard.nofix=Punkt bez fix-a +dialog.gpsbabel.filter.discard.unknownfix=Punkt z nieznanym fix-em +dialog.gpsbabel.filter.simplify.intro=Usuwaj punkty dop\u00f3ki +dialog.gpsbabel.filter.simplify.maxpoints=Ilo\u015b\u0107 punkt\u00f3w < +dialog.gpsbabel.filter.simplify.maxerror=lub b\u0142\u0105d odleg\u0142o\u015bci +dialog.gpsbabel.filter.simplify.crosstrack=skrzy\u017cowane \u015bcie\u017cki +dialog.gpsbabel.filter.simplify.length=d\u0142ugo\u015b\u0107 r\u00f3\u017cnicy +dialog.gpsbabel.filter.simplify.relative=powi\u0105zan z Hdop +dialog.gpsbabel.filter.distance.intro=Usu\u0144 punkty je\u015bli poprzednie punkt +dialog.gpsbabel.filter.distance.distance=Je\u015bli odleg\u0142o\u015b\u0107 < +dialog.gpsbabel.filter.distance.time=i r\u00f3\u017cnica w czasie < +dialog.gpsbabel.filter.interpolate.intro=Dodaj ekstra punkty pomi\u0119dzy punktami \u015bcie\u017cki +dialog.gpsbabel.filter.interpolate.distance=Je\u015bli odleg\u0142o\u015b\u0107 > +dialog.gpsbabel.filter.interpolate.time=lub r\u00f3\u017cnica czasu > dialog.saveoptions.title=Zapisz plik dialog.save.fieldstosave=Pola do zapisu dialog.save.table.field=Pole @@ -192,7 +219,10 @@ dialog.exportkml.text=Tytu\u0142 dla danych dialog.exportkml.altitude=Do\u0142\u0105cz wysoko\u015bci (dla cel\u00f3w lotniczych) dialog.exportkml.kmz=Skompresuj do pliku KMZ dialog.exportkml.exportimages=Eksportuj miniaturki zdj\u0119\u0107 do KMZ +dialog.exportkml.imagesize=Rozmiar zdj\u0119\u0107 dialog.exportkml.trackcolour=Kolor \u015bcie\u017cki +dialog.exportkml.standardkml=Standardowy KML +dialog.exportkml.extendedkml=Standardowy KML ze znacznikami czasu (no, *extended*, not standard!) dialog.exportgpx.name=Nazwa dialog.exportgpx.desc=Opis dialog.exportgpx.includetimestamps=Do\u0142\u0105cz znaczniki czasu @@ -208,11 +238,23 @@ dialog.exportpov.cameraz=Kamera Z dialog.exportpov.modelstyle=Styl modelu dialog.exportpov.ballsandsticks=Kule i pa\u0142ki dialog.exportpov.tubesandwalls=Rurki i \u015bciany -dialog.exportpov.warningtracksize=Ta \u015bcie\u017cka ma bardzo wiele punkt\u00f3w, kt\u00f3rych Java3D mo\u017ce nie wy\u015bwietli\u0107.\nCzy chcesz kontynuowa\u0107? +dialog.3d.warningtracksize=Ta \u015bcie\u017cka ma bardzo wiele punkt\u00f3w, kt\u00f3rych Java3D mo\u017ce nie wy\u015bwietli\u0107.\nCzy chcesz kontynuowa\u0107? +dialog.exportpov.baseimage=Obraz podk\u0142adu +dialog.exportpov.cannotmakebaseimage=Nie mo\u017cna zapisa\u0107 obrazu podk\u0142adu +dialog.baseimage.title=Obrazu podk\u0142adu +dialog.baseimage.useimage=U\u017cyj obrazu +dialog.baseimage.mapsource=\u0179r\u00f3d\u0142o map +dialog.baseimage.zoom=Poziom zbli\u017cenia +dialog.baseimage.incomplete=Obraz niekompletny +dialog.baseimage.tiles=Kafelki +dialog.baseimage.size=Rozmiar obrazu dialog.exportsvg.text=Wybierz parametry eksportu do pliku SVG dialog.exportsvg.phi=azymut \u03d5 dialog.exportsvg.theta=K\u0105t wzniesienia \u03b8 dialog.exportsvg.gradients=U\u017cyj gradientu jako wype\u0142nienia +dialog.exportimage.noimagepossible=Obrazy map musz\u0105 zosta\u0107 zapisane na dysku przed ich eksportem +dialog.exportimage.drawtrack=Rysuj \u015bcie\u017ck\u0119 na mapie +dialog.exportimage.textscalepercent=Wsp\u00f3\u0142czynnik skali tekstu (%) dialog.pointtype.desc=Zapisz punkty nast\u0119puj\u0105cych typ\u00f3w: dialog.pointtype.track=punkty \u015bcie\u017cki dialog.pointtype.waypoint=punkty po\u015brednie @@ -233,7 +275,9 @@ dialog.clearundo.title=Wyczy\u015b\u0107 list\u0119 zmian dialog.clearundo.text=Czy na pewno chcesz wyczy\u015bci\u0107 list\u0119 zmian?\nWszystkie informacje o zmianach b\u0119d\u0105 utracone! dialog.pointedit.title=Edytuj punkt dialog.pointedit.text=Zaznacz wszystkie pola do edycji i u\u017cyj przycisku 'Edytuj' by zmieni\u0107 warto\u015bci +dialog.pointedit.intro=Zaznacz wszystkie pola by zmieni\u0107 warto\u015bci dialog.pointedit.table.field=Pole +dialog.pointedit.nofield=Nie wybrano \u017cadnego pola dialog.pointedit.table.value=Warto\u015b\u0107 dialog.pointedit.table.changed=Zmieniony dialog.pointedit.changevalue.text=Wprowad\u017a now\u0105 warto\u015b\u0107 tego pola @@ -279,6 +323,17 @@ dialog.distances.toofewpoints=Ta funkcja wymaga przynajmniej dw\u00f3ch punkt\u0 dialog.fullrangedetails.intro=Szczeg\u00f3\u0142y wybranego zakresu dialog.fullrangedetails.coltotal=Z lukami dialog.fullrangedetails.colsegments=Bez luk +dialog.estimatetime.details=Szczeg\u00f3\u0142y +dialog.estimatetime.gentle=\u0141agodnie +dialog.estimatetime.steep=Stromo +dialog.estimatetime.parameters=Parametry +dialog.estimatetime.parameters.timefor=Czas dla +dialog.estimatetime.results=Wynik +dialog.estimatetime.results.estimatedtime=Czas przewidywany +dialog.estimatetime.results.actualtime=Czas bie\u017c\u0105cy +dialog.learnestimationparams.averageerror=B\u0142\u0105d \u015bredni +dialog.learnestimationparams.weight.current=bie\u017c\u0105ce +dialog.learnestimationparams.weight.calculated=obliczone dialog.setmapbg.intro=Wybierz dostawc\u0119 map t\u0142a lub dodaj nowego dialog.addmapsource.title=Dodaj dostawc\u0119 map dialog.addmapsource.sourcename=Nazwa dostawcy @@ -478,9 +533,6 @@ dialog.searchwikipedianames.search=Szukaj # 3d window dialog.3d.title=GpsPrune widok tr\u00f3jwymiarowy dialog.3d.altitudefactor=Wsp\u00f3\u0142czynnik skalowania wysoko\u015bci -dialog.3dlines.title=Linie siatki -dialog.3dlines.empty=Brak siatki do wy\u015bwietlenia! -dialog.3dlines.intro=Linie siatki w widoku 3D # Confirm messages confirm.loadfile=Za\u0142adowano dane z pliku @@ -529,7 +581,6 @@ button.cancel=Anuluj button.overwrite=Nadpisz button.moveup=Do g\u00f3ry button.movedown=Na d\u00f3\u0142 -button.showlines=Poka\u017c linie button.edit=Edycja button.exit=Zako\u0144cz button.close=Zamknij @@ -552,6 +603,7 @@ button.browse=Przegl\u0105daj... button.addnew=Dodaj nowy button.delete=Usu\u0144 button.manage=Zarz\u0105dzaj +button.combine=Po\u0142\u0105cz # File types filetype.txt=Pliki TXT @@ -562,12 +614,14 @@ filetype.kmz=Pliki KMZ filetype.gpx=Pliki GPX filetype.pov=Pliki POV filetype.svg=Pliki SVG +filetype.png=Pliki PNG filetype.audio=Pliki MP3, OGG, WAV # Display components display.nodata=Nie za\u0142adowano danych display.noaltitudes=\u015acie\u017cki nie zawieraj\u0105 informacji o wysoko\u015bci display.notimestamps=\u015acie\u017cki nie zawieraj\u0105 informacji o czasie +display.novalues=\u015acie\u017cki nie zawieraj\u0105 warto\u015bci dla tego pola details.trackdetails=Szczeg\u00f3\u0142y \u015bcie\u017cki details.notrack=Brak za\u0142adowanych \u015bcie\u017cek details.track.points=Punkty @@ -638,21 +692,31 @@ units.feet=Stopy units.feet.short=ft units.kilometres=Kilometry units.kilometres.short=km +units.kilometresperhour=km na godzin\u0119 units.kilometresperhour.short=km/h units.miles=Mile units.miles.short=mi +units.milesperhour=mil na godzin\u0119 units.milesperhour.short=mi/h units.nauticalmiles=Mile morskie units.nauticalmiles.short=Mm units.nauticalmilesperhour.short=w. +units.metrespersec=metry (metr\u00f3w) na sekund\u0119 units.metrespersec.short=m/s +units.feetpersec=stopy (st\u00f3p) na sekund\u0119 units.feetpersec.short=ft/s units.hours=Godziny +units.minutes=minuty +units.seconds=sekundy units.degminsec=Sto-min-sek units.degmin=Sto-min units.deg=Stopnie units.iso8601=ISO 8601 +# How to combine conditions, such as filters +logic.and=i +logic.or=lub + # External urls url.googlemaps=maps.google.pl wikipedia.lang=pl @@ -721,7 +785,7 @@ error.undofailed.text=Nie mo\u017cna cofn\u0105\u0107 error.function.noop.title=Funkcja nie ma skutku error.rearrange.noop=Przestawienie punkt\u00f3w nie przyniesie skutku error.function.notavailable.title=Funkcja nie dost\u0119pna -error.function.nojava3d=Ta funkcja wymaga biblioteki Java3d,\ndost\u0119pnej na Sun.com. +error.function.nojava3d=Ta funkcja wymaga biblioteki Java3d. error.3d=Nast\u0105pi\u0142 b\u0142\u0105d z wy\u015bwietlaniem 3D error.readme.notfound=Nie znaleziono pliku Readme error.osmimage.dialogtitle=B\u0142\u0105d przy \u0142adowaniu obraz\u00f3w map diff --git a/tim/prune/lang/prune-texts_pt.properties b/tim/prune/lang/prune-texts_pt.properties index 1220049..1d0e3ef 100644 --- a/tim/prune/lang/prune-texts_pt.properties +++ b/tim/prune/lang/prune-texts_pt.properties @@ -97,7 +97,6 @@ function.show3d=Visualizar 3D function.distances=Dist\u00e2ncias function.fullrangedetails=Todos os detalhes function.setmapbg=Definir como fundo do mapa -function.setkmzimagesize=Definir tamanho da imagem KMZ function.setpaths=Definir caminhos do programa function.getgpsies=Obter rotas Gpsies function.uploadgpsies=Enviar rotas para o Gpsies @@ -204,7 +203,8 @@ dialog.exportpov.cameraz=Z da C\u00e2mera dialog.exportpov.modelstyle=Estilo do modelo dialog.exportpov.ballsandsticks=Bolas e galhos dialog.exportpov.tubesandwalls=Tubos e muros -dialog.exportpov.warningtracksize=Esta rota possui um grande n\u00famero de pontos, os quais o Java3D n\u00e3o ser\u00e1 capaz de exibir.\n Voc\u00ea tem certeza que deseja continuar? +dialog.3d.warningtracksize=Esta rota possui um grande n\u00famero de pontos, os quais o Java3D n\u00e3o ser\u00e1 capaz de exibir.\n Voc\u00ea tem certeza que deseja continuar? +dialog.exportkml.imagesize=Tamanho da imagem dialog.exportsvg.text=Selecione os par\u00e2metros para a exporta\u00e7\u00e3o para o SVG dialog.exportsvg.phi=\u00c2ngulo do azimute \u03d5 dialog.exportsvg.theta=\u00c2ngulo da eleva\u00e7\u00e3o \u03b8 @@ -465,9 +465,6 @@ dialog.searchwikipedianames.search=Procurar por: # 3d window dialog.3d.title=Vista 3D do GpsPrune dialog.3d.altitudefactor=Fator de exagera\u00e7\u00e3o de altitude -dialog.3dlines.title=Linhas da grade do GpsPrune -dialog.3dlines.empty=Nenhuma linha de grade para exibir! -dialog.3dlines.intro=Estas s\u00e3o as linhas da grade para a vista 3D. # Confirm messages confirm.loadfile=Dados carregados do arquivo @@ -515,7 +512,6 @@ button.cancel=Cancelar button.overwrite=Sobrescrever button.moveup=Mover acima button.movedown=Mover abaixo -button.showlines=Mostrar linhas button.edit=Editar button.exit=Sair button.close=Fechar @@ -702,7 +698,7 @@ error.undofailed.text=Falha para desfazer opera\u00e7\u00e3o error.function.noop.title=Fun\u00e7\u00e3o sem nenhum efeito error.rearrange.noop=Rearruma\u00e7\u00e3o de pontos n\u00e3o teve efeito error.function.notavailable.title=Fun\u00e7\u00e3o n\u00e3o dispon\u00edvel -error.function.nojava3d=Esta fun\u00e7\u00e3o precisa da biblioteca Java3d,\ndispon\u00edvel em Sun.com +error.function.nojava3d=Esta fun\u00e7\u00e3o precisa da biblioteca Java3d error.3d=Um erro ocorreu com a exibi\u00e7\u00e3o 3D error.readme.notfound=Arquivo Leiame n\u00e3o encontrado error.osmimage.dialogtitle=Erro ao carregar imagens do mapa diff --git a/tim/prune/lang/prune-texts_ru.properties b/tim/prune/lang/prune-texts_ru.properties index c169538..fc85128 100644 --- a/tim/prune/lang/prune-texts_ru.properties +++ b/tim/prune/lang/prune-texts_ru.properties @@ -100,7 +100,6 @@ function.show3d=3D-\u0432\u0438\u0434 function.distances=\u0420\u0430\u0441\u0441\u0442\u043e\u044f\u043d\u0438\u044f function.fullrangedetails=\u0414\u0435\u0442\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u043e \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u0443 function.setmapbg=\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u043a\u0430\u0440\u0442\u0443-\u043f\u043e\u0434\u043b\u043e\u0436\u043a\u0443 -function.setkmzimagesize=\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0440\u0430\u0437\u043c\u0435\u0440 KMZ-\u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f function.setpaths=\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u0443\u0442\u0438 \u043a \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0430\u043c function.getgpsies=\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0442\u0440\u0435\u043a\u0438 function.uploadgpsies=\u0412\u044b\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0442\u0440\u0435\u043a \u043d\u0430 gpsies.com @@ -208,7 +207,7 @@ dialog.exportpov.cameraz=\u041a\u0430\u043c\u0435\u0440\u0430 Z dialog.exportpov.modelstyle=\u0421\u0442\u0438\u043b\u044c \u043c\u043e\u0434\u0435\u043b\u0438 dialog.exportpov.ballsandsticks=\u041c\u044f\u0447\u0438 \u0438 \u043f\u0430\u043b\u043e\u0447\u043a\u0438 dialog.exportpov.tubesandwalls=\u0422\u0440\u0443\u0431\u044b \u0438 \u0441\u0442\u0435\u043d\u044b -dialog.exportpov.warningtracksize=\u0412\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0432 \u0442\u0440\u0435\u043a\u0435 \u0431\u043e\u043b\u044c\u0448\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0442\u043e\u0447\u0435\u043a - Java3D \u043c\u043e\u0436\u0435\u0442 \u0435\u0433\u043e \u043d\u0435 \u043e\u0442\u043e\u0431\u0440\u0430\u0437\u0438\u0442\u044c!\n\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c? +dialog.3d.warningtracksize=\u0412\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0432 \u0442\u0440\u0435\u043a\u0435 \u0431\u043e\u043b\u044c\u0448\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0442\u043e\u0447\u0435\u043a - Java3D \u043c\u043e\u0436\u0435\u0442 \u0435\u0433\u043e \u043d\u0435 \u043e\u0442\u043e\u0431\u0440\u0430\u0437\u0438\u0442\u044c!\n\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c? dialog.exportsvg.text=\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0434\u043b\u044f \u044d\u043a\u0441\u043f\u043e\u0440\u0442\u0430 SVG dialog.exportsvg.phi=\u0410\u0437\u0438\u043c\u0443\u0442 \u03d5 dialog.exportsvg.theta=\u0423\u0433\u043e\u043b \u03b8 @@ -230,7 +229,7 @@ dialog.undo.pretext=\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430 dialog.undo.none.title=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c dialog.undo.none.text=\u041d\u0435\u0442 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0434\u043b\u044f \u043e\u0442\u043c\u0435\u043d\u044b dialog.clearundo.title=\u041e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0434\u043b\u044f \u043e\u0442\u043c\u0435\u043d\u044b -dialog.clearundo.text=\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0434\u043b\u044f \u043e\u0442\u043c\u0435\u043d\u044b? n\u0421\u043f\u0438\u0441\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0431\u0443\u0434\u0435\u0442 \u043e\u0447\u0438\u0449\u0435\u043d \u043d\u0430\u0432\u0441\u0435\u0433\u0434\u0430! +dialog.clearundo.text=\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0434\u043b\u044f \u043e\u0442\u043c\u0435\u043d\u044b?\n\u0421\u043f\u0438\u0441\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0431\u0443\u0434\u0435\u0442 \u043e\u0447\u0438\u0449\u0435\u043d \u043d\u0430\u0432\u0441\u0435\u0433\u0434\u0430! dialog.pointedit.title=\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0442\u043e\u0447\u043a\u0443 dialog.pointedit.text=\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u043e\u043b\u0435 \u0434\u043b\u044f \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 «\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c» \u0434\u043b\u044f \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f dialog.pointedit.table.field=\u041f\u043e\u043b\u0435 @@ -478,9 +477,6 @@ dialog.searchwikipedianames.search=\u041f\u043e\u0438\u0441\u043a \u0434\u043b\u # 3d window dialog.3d.title=GpsPrune 3D-\u0432\u0438\u0434 dialog.3d.altitudefactor=\u041a\u043e\u044d\u0444\u0444\u0438\u0446\u0438\u0435\u043d\u0442 \u043f\u043e \u0432\u044b\u0441\u043e\u0442\u0435 -dialog.3dlines.title=\u0421\u0435\u0442\u043a\u0430 GpsPrune -dialog.3dlines.empty=\u041d\u0435\u0442 \u0441\u0435\u0442\u043a\u0438 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f! -dialog.3dlines.intro=\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0441\u0435\u0442\u043a\u0438 3D-\u0432\u0438\u0434\u0430 # Confirm messages confirm.loadfile=\u0414\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u044b @@ -529,7 +525,6 @@ button.cancel=\u041e\u0442\u043c\u0435\u043d\u0430 button.overwrite=\u041f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u0442\u044c button.moveup=\u041f\u0435\u0440\u0435\u043c\u0435\u0441\u0442\u0438\u0442\u044c \u0432\u0432\u0435\u0440\u0445 button.movedown=\u041f\u0435\u0440\u0435\u043c\u0435\u0441\u0442\u0438\u0442\u044c \u0432\u043d\u0438\u0437 -button.showlines=\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043b\u0438\u043d\u0438\u0438 button.edit=\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c button.exit=\u0412\u044b\u0445\u043e\u0434 button.close=\u0417\u0430\u043a\u0440\u044b\u0442\u044c @@ -562,6 +557,7 @@ filetype.kmz=KMZ \u0444\u0430\u0439\u043b\u044b filetype.gpx=GPX \u0444\u0430\u0439\u043b\u044b filetype.pov=POV \u0444\u0430\u0439\u043b\u044b filetype.svg=SVG \u0444\u0430\u0439\u043b\u044b +filetype.png=PNG \u0444\u0430\u0439\u043b\u044b filetype.audio=MP3, OGG, WAV \u0444\u0430\u0439\u043b\u044b # Display components diff --git a/tim/prune/lang/prune-texts_tr.properties b/tim/prune/lang/prune-texts_tr.properties index d098366..da661a9 100644 --- a/tim/prune/lang/prune-texts_tr.properties +++ b/tim/prune/lang/prune-texts_tr.properties @@ -87,7 +87,6 @@ function.show3d=3B g\u00fcr\u00fcnt\u00fcs\u00fc function.distances=Uzakl\u0131klar function.fullrangedetails=S\u0131ran\u0131n b\u00fct\u00fcn ayr\u0131nt\u0131lar function.setmapbg=Arkafonun haritas\u0131 se\u00e7 -function.setkmzimagesize=KMZ resim boyutu ayarla function.setpaths=Uygulamalar\u0131n yollar\u0131 ayarla function.getgpsies=Gpsies.com'dan yolu al function.duplicatepoint=Noktay\u0131 kopyala @@ -157,6 +156,7 @@ dialog.exportkml.text=Verinin ba\u015fl\u0131\u011f\u0131 dialog.exportkml.altitude=Absolut y\u00fckseklikleri (u\u00e7u\u015f i\u00e7in) dialog.exportkml.kmz=kmz dosyas\u0131 olu\u015fturmak i\u00e7in s\u0131k\u0131\u015ft\u0131r dialog.exportkml.exportimages=Fotolar\u0131n t\u0131rnak resimleri kmz dosyada dahil et +dialog.exportkml.imagesize=Resim boyutu dialog.exportkml.trackcolour=\u0130z rengi dialog.exportgpx.name=Ad\u0131 dialog.exportgpx.desc=A\u00e7\u0131klama @@ -303,7 +303,6 @@ button.cancel=\u0130ptal button.overwrite=\u00dczerinde yaz button.moveup=Yukar\u0131 button.movedown=A\u015fa\u011f\u0131 -button.showlines=Çizgiler g\u00f6r\u00fcnt\u00fcle button.edit=D\u00fczenle button.exit=Ç\u0131k\u0131\u015f button.close=Kapat diff --git a/tim/prune/lang/prune-texts_zh.properties b/tim/prune/lang/prune-texts_zh.properties index 6068744..d193691 100644 --- a/tim/prune/lang/prune-texts_zh.properties +++ b/tim/prune/lang/prune-texts_zh.properties @@ -84,6 +84,7 @@ function.exportkml=\u8f93\u51faKML\u6587\u4ef6 function.exportgpx=\u8f93\u51faGPX\u6587\u4ef6 function.exportpov=\u8f93\u51faPOV\u6587\u4ef6 function.exportsvg=\u8f93\u51faSVG\u6587\u4ef6 +function.exportimage=\u8f93\u51fa\u56fe\u50cf function.editwaypointname=\u7f16\u8f91\u822a\u70b9\u540d function.compress=\u538b\u7f29\u8f68\u8ff9(\u6807\u8bb0\u8981\u5220\u9664\u822a\u70b9) function.deleterange=\u5220\u9664\u8f68\u8ff9\u70b9\u6bb5 @@ -92,15 +93,16 @@ function.interpolate=\u91cd\u53e0\u8f68\u8ff9\u70b9 function.addtimeoffset=\u52a0\u5165\u65f6\u95f4\u5dee function.addaltitudeoffset=\u52a0\u5165\u9ad8\u5ea6\u504f\u79fb function.convertnamestotimes=\u822a\u70b9\u540d\u79f0\u8f6c\u4e3a\u65f6\u95f4 -function.deletefieldvalues=\u5220\u9664\u533a\u57df\u6570\u503c +function.deletefieldvalues=\u5220\u9664\u5b57\u6bb5\u503c function.findwaypoint=\u67e5\u627e\u822a\u70b9 function.pastecoordinates=\u8f93\u5165\u65b0\u5750\u6807 function.charts=\u56fe\u8868 function.show3d=3D\u89c6\u56fe function.distances=\u8ddd\u79bb function.fullrangedetails=\u5168\u822a\u6bb5\u8be6\u7ec6\u4fe1\u606f +function.estimatetime=\u4f30\u8ba1\u65f6\u95f4 +function.learnestimationparams=\u4f7f\u7528\u5f53\u524d\u8f68\u8ff9\u53c2\u6570\u4f30\u8ba1\u65f6\u95f4 function.setmapbg=\u80cc\u666f\u5730\u56fe -function.setkmzimagesize=\u8bbe\u7f6eKMZ\u56fe\u50cf\u5c3a\u5bf8 function.setpaths=\u8bbe\u7f6e\u7a0b\u5e8f\u8def\u5f84 function.getgpsies=\u83b7\u53d6Gpsies\u8f68\u8ff9 function.uploadgpsies=\u4e0a\u4f20\u8f68\u8ff9\u5230Gpsies @@ -146,10 +148,10 @@ dialog.deletephoto.deletepoint=\u5220\u9664\u94fe\u63a5\u5230\u7167\u7247\u7684\ dialog.deleteaudio.deletepoint=\u5220\u9664\u94fe\u63a5\u5230\u97f3\u9891\u7684\u8f68\u8ff9\u70b9\uff1f dialog.openoptions.title=\u6253\u5f00\u9009\u9879 dialog.openoptions.filesnippet=\u63d0\u53d6\u6587\u4ef6\u7247\u6bb5 -dialog.load.table.field=\u6570\u636e\u6bb5 +dialog.load.table.field=\u5b57\u6bb5 dialog.load.table.datatype=\u6570\u636e\u7c7b\u578b dialog.load.table.description=\u63cf\u8ff0 -dialog.delimiter.label=\u6570\u636e\u6bb5\u5206\u9694\u7b26 +dialog.delimiter.label=\u5b57\u6bb5\u5206\u9694\u7b26 dialog.delimiter.comma=\u9017\u53f7 dialog.delimiter.tab=Tab dialog.delimiter.space=\u7a7a\u683c @@ -159,6 +161,10 @@ dialog.openoptions.deliminfo.records=\u6761\u8bb0\u5f55\uff0c dialog.openoptions.deliminfo.fields=\u6570\u636e\u6bb5 dialog.openoptions.deliminfo.norecords=\u65e0\u7eaa\u5f55 dialog.openoptions.altitudeunits=\u9ad8\u5ea6\u5355\u4f4d +dialog.openoptions.speedunits=\u901f\u5ea6\u5355\u4f4d +dialog.openoptions.vertspeedunits=\u5782\u76f4\u901f\u5ea6\u5355\u4f4d +dialog.openoptions.vspeed.positiveup=\u4e0a\u5347\u4e3a\u6b63 +dialog.openoptions.vspeed.positivedown=\u4e0b\u964d\u4e3a\u6b63 dialog.open.contentsdoubled=\u6587\u4ef6\u542b\u6709\u4e24\u5957\u70b9\u4fe1\u606f\uff0c\u4e00\u5957\u822a\u70b9\u4fe1\u606f\u548c\u4e00\u5957\u8f68\u8ff9\u70b9\u4fe1\u606f dialog.selecttracks.intro=\u9009\u62e9\u8981\u5bfc\u5165\u7684\u8f68\u8ff9 dialog.selecttracks.noname=\u672a\u547d\u540d @@ -176,6 +182,30 @@ dialog.gpsload.save=\u4fdd\u5b58\u5230\u6587\u4ef6 dialog.gpssend.sendwaypoints=\u53d1\u9001\u822a\u70b9 dialog.gpssend.sendtracks=\u53d1\u9001\u8f68\u8ff9 dialog.gpssend.trackname=\u8f68\u8ff9\u540d +dialog.gpsbabel.filters=\u7b5b\u9009\u6761\u4ef6 +dialog.addfilter.title=\u6dfb\u52a0\u7b5b\u9009\u6761\u4ef6 +dialog.gpsbabel.filter.discard=\u820d\u5f03 +dialog.gpsbabel.filter.simplify=\u7b80\u5316 +dialog.gpsbabel.filter.distance=\u8ddd\u79bb +dialog.gpsbabel.filter.interpolate=\u63d2\u503c +dialog.gpsbabel.filter.discard.intro=\u820d\u5f03\u8f68\u8ff9\u70b9\u5f53 +dialog.gpsbabel.filter.discard.hdop=Hdop > +dialog.gpsbabel.filter.discard.vdop=Vdop > +dialog.gpsbabel.filter.discard.numsats=\u536b\u661f\u6570 < +dialog.gpsbabel.filter.discard.nofix=\u8f68\u8ff9\u70b9\u672a\u5b9a\u4f4d +dialog.gpsbabel.filter.discard.unknownfix=\u8f68\u8ff9\u70b9\u5b9a\u4f4d\u4e0d\u660e +dialog.gpsbabel.filter.simplify.intro=\u5220\u9664\u6240\u6709\u8f68\u8ff9\u70b9\u76f4\u5230 +dialog.gpsbabel.filter.simplify.maxpoints=\u8f68\u8ff9\u70b9\u6570 < +dialog.gpsbabel.filter.simplify.maxerror=\u6216\u8bef\u5dee\u8ddd\u79bb < +dialog.gpsbabel.filter.simplify.crosstrack=\u504f\u822a +dialog.gpsbabel.filter.simplify.length=\u957f\u5ea6\u5dee +dialog.gpsbabel.filter.simplify.relative=\u76f8\u5bf9hdop +dialog.gpsbabel.filter.distance.intro=\u79fb\u9664\u4e0e\u524d\u4e00\u70b9\u8ddd\u79bb\u592a\u8fd1\u7684\u8f68\u8ff9\u70b9 +dialog.gpsbabel.filter.distance.distance=\u5f53\u8ddd\u79bb < +dialog.gpsbabel.filter.distance.time=\u4e14\u65f6\u95f4\u5dee < +dialog.gpsbabel.filter.interpolate.intro=\u63d2\u5165\u4e2d\u95f4\u70b9 +dialog.gpsbabel.filter.interpolate.distance=\u5f53\u8ddd\u79bb > +dialog.gpsbabel.filter.interpolate.time=\u6216\u65f6\u95f4\u5dee > dialog.saveoptions.title=\u4fdd\u5b58 dialog.save.fieldstosave=\u4fdd\u5b58\u6570\u636e\u6bb5 dialog.save.table.field=\u6570\u636e\u6bb5 @@ -192,7 +222,10 @@ dialog.exportkml.text=\u6570\u636e\u540d\u79f0 dialog.exportkml.altitude=\u7edd\u5bf9\u9ad8\u5ea6(\u822a\u7a7a\u7528) dialog.exportkml.kmz=\u538b\u7f29\u6210KMZ\u6587\u4ef6 dialog.exportkml.exportimages=\u8f93\u51fa\u7f29\u7565\u56fe\u81f3KMZ +dialog.exportkml.imagesize=\u56fe\u50cf\u5c3a\u5bf8 dialog.exportkml.trackcolour=\u8f68\u8ff9\u989c\u8272 +dialog.exportkml.standardkml=\u6807\u51c6KML +dialog.exportkml.extendedkml=\u5e26\u65f6\u95f4\u6233\u8bb0\u7684\u6269\u5c55KML dialog.exportgpx.name=\u540d\u79f0 dialog.exportgpx.desc=\u63cf\u8ff0 dialog.exportgpx.includetimestamps=\u5305\u542b\u65f6\u95f4 @@ -208,11 +241,23 @@ dialog.exportpov.cameraz=\u76f8\u673aZ\u5750\u6807 dialog.exportpov.modelstyle=\u6a21\u578b\u7c7b\u578b dialog.exportpov.ballsandsticks=\u7403\u6746\u6a21\u578b dialog.exportpov.tubesandwalls=\u7ba1\u5899\u6a21\u578b -dialog.exportpov.warningtracksize=\u8f68\u8ff9\u542b\u6709\u592a\u591a\u822a\u70b9\uff0cJAVA3D\u53ef\u80fd\u65e0\u6cd5\u663e\u793a\u3002\n\u662f\u5426\u7ee7\u7eed\uff1f +dialog.3d.warningtracksize=\u8f68\u8ff9\u542b\u6709\u592a\u591a\u822a\u70b9\uff0cJAVA3D\u53ef\u80fd\u65e0\u6cd5\u663e\u793a\u3002\n\u662f\u5426\u7ee7\u7eed\uff1f +dialog.exportpov.baseimage=\u57fa\u7840\u56fe +dialog.exportpov.cannotmakebaseimage=\u65e0\u6cd5\u4fdd\u5b58\u57fa\u7840\u56fe +dialog.baseimage.title=\u8bbe\u7f6e\u57fa\u7840\u56fe +dialog.baseimage.useimage=\u4f7f\u7528\u6b64\u56fe\u50cf +dialog.baseimage.mapsource=\u5730\u56fe\u6e90 +dialog.baseimage.zoom=\u7f29\u653e\u7ea7\u522b +dialog.baseimage.incomplete=\u56fe\u50cf\u4e0d\u5b8c\u6574 +dialog.baseimage.tiles=\u5730\u56fe\u5757 +dialog.baseimage.size=\u56fe\u50cf\u5c3a\u5bf8 dialog.exportsvg.text=\u9009\u62e9\u8f93\u51faSVG\u6587\u4ef6\u7684\u53c2\u6570 dialog.exportsvg.phi=\u65b9\u4f4d\u89d2 dialog.exportsvg.theta=\u4ef0\u89d2 dialog.exportsvg.gradients=\u4f7f\u7528\u6e10\u53d8\u8272 +dialog.exportimage.noimagepossible=\u8f93\u51fa\u7684\u5730\u56fe\u56fe\u50cf\u9996\u5148\u9700\u8981\u7f13\u5b58\u5728\u672c\u5730\u78c1\u76d8\u4e0a +dialog.exportimage.drawtrack=\u7ed8\u51fa\u8f68\u8ff9 +dialog.exportimage.textscalepercent=\u6587\u5b57\u7f29\u653e\u6bd4\u4f8b (%) dialog.pointtype.desc=\u4fdd\u5b58\u4e0b\u5217\u70b9\uff1a dialog.pointtype.track=\u8f68\u8ff9\u70b9 dialog.pointtype.waypoint=\u822a\u70b9 @@ -232,12 +277,10 @@ dialog.undo.none.text=\u65e0\u64cd\u4f5c\u53ef\u64a4\u9500 dialog.clearundo.title=\u6e05\u9664\u64a4\u9500\u64cd\u4f5c\u6e05\u5355 dialog.clearundo.text=\u662f\u5426\u786e\u5b9e\u8981\u6e05\u9664\u64a4\u9500\u64cd\u4f5c\u6e05\u5355\uff1f\n\u64a4\u9500\u64cd\u4f5c\u4fe1\u606f\u4f1a\u4e22\u5931\uff01 dialog.pointedit.title=\u7f16\u8f91\u8f68\u8ff9\u70b9 -dialog.pointedit.text=\u9009\u62e9\u8981\u7f16\u8f91\u7684\u533a\u57df\u5e76\u7528\u201c\u7f16\u8f91\u201d\u952e\u6539\u53d8\u6570\u503c -dialog.pointedit.table.field=\u6570\u636e\u6bb5 +dialog.pointedit.intro=\u4f9d\u6b21\u9009\u4e2d\u5b57\u6bb5\u4ee5\u67e5\u770b\u53ca\u8c03\u6574\u6570\u503c +dialog.pointedit.table.field=\u5b57\u6bb5 +dialog.pointedit.nofield=\u672a\u9009\u4e2d\u5b57\u6bb5 dialog.pointedit.table.value=\u6570\u503c -dialog.pointedit.table.changed=\u5df2\u6539\u53d8 -dialog.pointedit.changevalue.text=\u8f93\u5165\u65b0\u6570\u503c -dialog.pointedit.changevalue.title=\u7f16\u8f91\u6570\u636e\u6bb5 dialog.pointnameedit.name=\u822a\u70b9\u540d\u79f0 dialog.pointnameedit.uppercase=\u4e0a\u6863\u952e dialog.pointnameedit.lowercase=\u4e0b\u6863\u952e @@ -279,6 +322,27 @@ dialog.distances.toofewpoints=\u9700\u8981\u822a\u70b9\u6765\u8ba1\u7b97\u8ddd\u dialog.fullrangedetails.intro=\u822a\u6bb5\u8be6\u60c5 dialog.fullrangedetails.coltotal=\u5305\u542b\u95f4\u65ad dialog.fullrangedetails.colsegments=\u4e0d\u5305\u542b\u95f4\u65ad +dialog.estimatetime.details=\u8be6\u60c5 +dialog.estimatetime.gentle=\u5e73\u7f13 +dialog.estimatetime.steep=\u9661\u5ced +dialog.estimatetime.climb=\u4e0a\u5347 +dialog.estimatetime.descent=\u4e0b\u964d +dialog.estimatetime.parameters=\u53c2\u6570 +dialog.estimatetime.parameters.timefor=\u9700\u65f6 +dialog.estimatetime.results=\u7ed3\u679c +dialog.estimatetime.results.estimatedtime=\u4f30\u8ba1\u65f6\u95f4 +dialog.estimatetime.results.actualtime=\u5b9e\u9645\u7528\u65f6 +dialog.estimatetime.error.nodistance=\u4f30\u8ba1\u65f6\u95f4\u9700\u8981\u8fde\u7eed\u7684\u8f68\u8ff9 +dialog.estimatetime.error.noaltitudes=\u9009\u4e2d\u90e8\u5206\u4e0d\u5305\u542b\u9ad8\u5ea6\u4fe1\u606f +dialog.learnestimationparams.intro=\u6b64\u8f68\u8ff9\u8ba1\u7b97\u5f97\u5230\u7684\u53c2\u6570\u4e3a +dialog.learnestimationparams.averageerror=\u5e73\u5747\u8bef\u5dee +dialog.learnestimationparams.combine=\u8fd9\u4e9b\u53c2\u6570\u53ef\u4e0e\u5f53\u524d\u6570\u503c\u5408\u5e76 +dialog.learnestimationparams.combinedresults=\u5408\u5e76\u7ed3\u679c +dialog.learnestimationparams.weight.100pccurrent=\u4fdd\u6301\u5f53\u524d\u6570\u503c +dialog.learnestimationparams.weight.current=\u5f53\u524d\u503c +dialog.learnestimationparams.weight.calculated=\u8ba1\u7b97\u503c +dialog.learnestimationparams.weight.50pc=\u5e73\u5747\u503c +dialog.learnestimationparams.weight.100pccalculated=\u4f7f\u7528\u65b0\u8ba1\u7b97\u503c dialog.setmapbg.intro=\u8bf7\u9009\u62e9\u5730\u56fe\uff0c\u6216\u6dfb\u52a0\u5730\u56fe dialog.addmapsource.title=\u6dfb\u52a0\u5730\u56fe dialog.addmapsource.sourcename=\u5730\u56fe\u6765\u6e90\u540d\u79f0 @@ -364,7 +428,7 @@ dialog.deletemarked.nonefound=\u65e0\u6cd5\u5220\u9664\u6570\u636e\u70b9 dialog.pastecoordinates.desc=\u5728\u6b64\u8f93\u5165\u6216\u7c98\u8d34\u5750\u6807\u70b9 dialog.pastecoordinates.coords=\u5750\u6807\u70b9 dialog.pastecoordinates.nothingfound=\u8bf7\u68c0\u67e5\u5750\u6807\u6570\u636e\u5e76\u91cd\u8bd5 -dialog.help.help=\u66f4\u591a\u4fe1\u606f\u548c\u7528\u6cd5\uff0c\u8bf7\u53c2\u8003\u7f51\u7ad9\nhttp://activityworkshop.net/software/gpsprune/ +dialog.help.help=\u66f4\u591a\u4fe1\u606f\u548c\u7528\u6cd5\uff0c\u8bf7\u53c2\u8003\u7f51\u7ad9\nhttp://gpsprune.activityworkshop.net/// dialog.about.version=\u7248\u672c dialog.about.build=Build dialog.about.summarytext1=GpsPrune\u662f\u4e00\u4e2a\u4eceGPS\u4e2d\u5bfc\u5165\u6570\u636e\uff0c\u663e\u793a\u6570\u636e\u548c\u7f16\u8f91\u6570\u636e\u7684\u8f6f\u4ef6 @@ -403,7 +467,7 @@ dialog.checkversion.newversion1=\u53d1\u73b0\u65b0\u7248\u672c\uff01\u6700\u65b0 dialog.checkversion.newversion2= dialog.checkversion.releasedate1=\u65b0\u7248\u672c\u53d1\u884c\u4e8e dialog.checkversion.releasedate2= -dialog.checkversion.download=\u4e0b\u8f7d\u6700\u65b0\u7248\u672c\uff0c\u8bf7\u767b\u9646\u7f51\u7ad9\uff1a\nhttp:activityworkshop.net/software/gpsprune/download.html +dialog.checkversion.download=\u4e0b\u8f7d\u6700\u65b0\u7248\u672c\uff0c\u8bf7\u767b\u9646\u7f51\u7ad9\uff1a\nhttp:gpsprune.activityworkshop.net/download.html dialog.keys.intro=\u53ef\u7528\u4e0b\u5217\u5feb\u6377\u952e\u66ff\u4ee3\u9f20\u6807 dialog.keys.keylist=
\u7bad\u5934\u4e0a\u4e0b\u5de6\u53f3\u79fb\u52a8\u5730\u56fe
Ctrl + \u5de6\u53f3\u7bad\u5934\u9009\u53d6\u524d\uff0c\u540e\u70b9
Ctrl + \u4e0a\u4e0b\u7bad\u5934\u653e\u5927\u7f29\u5c0f
Ctrl + PgUp, PgDown\u9009\u62e9\u524d\u540e\u6bb5
Ctrl + Home, End\u9009\u62e9\u9996\u672b\u70b9
Del\u5220\u9664\u5f53\u524d\u70b9
dialog.keys.normalmodifier=Ctrl @@ -469,8 +533,8 @@ dialog.diskcache.maximumage=\u6700\u957f\u65f6\u95f4(\u5929) dialog.diskcache.deleteall=\u5220\u9664\u6240\u6709\u5730\u56fe\u5757 dialog.diskcache.deleted1=\u5df2\u5220\u9664 dialog.diskcache.deleted2=\u7f13\u5b58\u5185\u6587\u4ef6 -dialog.deletefieldvalues.intro=\u9009\u62e9\u5f53\u524d\u8303\u56f4\u5185\u8981\u5220\u9664\u7684\u6570\u636e -dialog.deletefieldvalues.nofields=\u9009\u5b9a\u8303\u56f4\u5185\u6ca1\u6709\u8981\u5220\u9664\u7684\u6570\u636e +dialog.deletefieldvalues.intro=\u9009\u62e9\u5f53\u524d\u8303\u56f4\u5185\u8981\u5220\u9664\u7684\u5b57\u6bb5 +dialog.deletefieldvalues.nofields=\u9009\u5b9a\u8303\u56f4\u5185\u6ca1\u6709\u8981\u5220\u9664\u7684\u5b57\u6bb5 dialog.setlinewidth.text=\u8f93\u5165\u8f68\u8ff9\u7ebf\u5bbd\u50cf\u7d20\u503c(1-4) dialog.downloadosm.desc=\u786e\u8ba4\u4eceOSM\u4e0b\u8f7d\u8be5\u5730\u533a\u539f\u59cb\u6570\u636e: dialog.searchwikipedianames.search=\u67e5\u627e: @@ -478,9 +542,6 @@ dialog.searchwikipedianames.search=\u67e5\u627e: # 3d window dialog.3d.title=GpsPrune 3D \u663e\u793a dialog.3d.altitudefactor=\u9ad8\u5ea6\u653e\u5927\u7cfb\u6570 -dialog.3dlines.title=GpsPrune \u7f51\u683c\u7ebf -dialog.3dlines.empty=\u65e0\u6cd5\u663e\u793a\u7f51\u683c\u7ebf -dialog.3dlines.intro=3D \u7f51\u683c\u7ebf # Confirm messages confirm.loadfile=\u6570\u636e\u5df2\u4ece\u6587\u4ef6\u5bfc\u5165 @@ -529,7 +590,6 @@ button.cancel=\u53d6\u6d88 button.overwrite=\u8986\u76d6 button.moveup=\u4e0a\u79fb button.movedown=\u4e0b\u79fb -button.showlines=\u663e\u793a\u7ebf\u6761 button.edit=\u7f16\u8f91 button.exit=\u9000\u51fa button.close=\u5173\u95ed @@ -552,6 +612,7 @@ button.browse=\u6d4f\u89c8... button.addnew=\u6dfb\u52a0 button.delete=\u5220\u9664 button.manage=\u7ba1\u7406 +button.combine=\u5408\u5e76 # File types filetype.txt=TXT\u6587\u4ef6 @@ -562,12 +623,14 @@ filetype.kmz=KMZ\u6587\u4ef6 filetype.gpx=GPX\u6587\u4ef6 filetype.pov=POV\u6587\u4ef6 filetype.svg=SVG\u6587\u4ef6 +filetype.png=PNG\u6587\u4ef6 filetype.audio=WAV,OGG,MP3\u6587\u4ef6 # Display components display.nodata=\u65e0\u6570\u636e display.noaltitudes=\u8f68\u8ff9\u6570\u636e\u4e0d\u542b\u9ad8\u5ea6\u4fe1\u606f display.notimestamps=\u8f68\u8ff9\u6570\u636e\u672a\u542b\u65f6\u95f4\u4fe1\u606f +display.novalues=\u8f68\u8ff9\u6570\u636e\u4e0d\u4fdd\u542b\u6b64\u5b57\u6bb5 details.trackdetails=\u8f68\u8ff9\u4fe1\u606f details.notrack=\u65e0\u8f68\u8ff9 details.track.points=\u8f68\u8ff9\u70b9 @@ -638,21 +701,31 @@ units.feet=\u82f1\u5c3a units.feet.short=\u82f1\u5c3a units.kilometres=\u5343\u7c73 units.kilometres.short=\u5343\u7c73 +units.kilometresperhour=\u5343\u7c73/\u65f6 units.kilometresperhour.short=\u5343\u7c73/\u65f6 units.miles=\u82f1\u91cc units.miles.short=\u82f1\u91cc +units.milesperhour=\u82f1\u91cc/\u65f6 units.milesperhour.short=\u82f1\u91cc/\u65f6 units.nauticalmiles=\u6d77\u91cc units.nauticalmiles.short=\u6d77\u91cc units.nauticalmilesperhour.short=\u6d77\u91cc/\u65f6 +units.metrespersec=\u7c73/\u79d2 units.metrespersec.short=\u7c73/\u79d2 +units.feetpersec=\u82f1\u5c3a/\u79d2 units.feetpersec.short=\u82f1\u5c3a/\u79d2 units.hours=\u5c0f\u65f6 +units.minutes=\u5206 +units.seconds=\u79d2 units.degminsec=\u5ea6-\u5206-\u79d2 units.degmin=\u5ea6-\u5206 units.deg=\u5ea6 units.iso8601=ISO 8601 +# How to combine conditions, such as filters +logic.and=\u4e0e +logic.or=\u6216 + # External urls url.googlemaps=ditu.google.cn wikipedia.lang=zh @@ -721,7 +794,7 @@ error.undofailed.text=\u64a4\u9500\u64cd\u4f5c\u5931\u8d25 error.function.noop.title=\u529f\u80fd\u65e0\u6548 error.rearrange.noop=\u91cd\u65b0\u914d\u7f6e\u822a\u70b9\u65e0\u6548 error.function.notavailable.title=\u65e0\u6b64\u529f\u80fd -error.function.nojava3d=\u6b64\u529f\u80fd\u9700\u8981 Java 3D\uff0c\u53ef\u4eceSun.com\u83b7\u5f97 +error.function.nojava3d=\u6b64\u529f\u80fd\u9700\u8981 Java 3D error.3d=3D \u663e\u793a\u9519\u8bef error.readme.notfound=\u627e\u4e0d\u5230\u7248\u672c\u4fe1\u606f\u6587\u4ef6 error.osmimage.dialogtitle=\u5bfc\u5165\u5730\u56fe\u65f6\u9519\u8bef @@ -738,3 +811,4 @@ error.cache.notthere=\u672a\u627e\u5230\u533a\u57df\u6570\u636e\u7f13\u5b58\u658 error.cache.empty=\u533a\u57df\u6570\u636e\u6587\u4ef6\u5939\u7a7a error.cache.cannotdelete=\u65e0\u53ef\u5220\u9664\u533a\u57df\u6570\u636e error.interpolate.invalidparameter=\u8f93\u5165\u70b9\u6570\u91cf\u5fc5\u987b\u57281\u52301000\u4e4b\u95f4 +error.learnestimationparams.failed=\u65e0\u6cd5\u4ece\u6b64\u8f68\u8ff9\u5f97\u5230\u53c2\u6570\u3002 \n \u5c1d\u8bd5\u8f7d\u5165\u66f4\u591a\u8f68\u8ff9\u3002 diff --git a/tim/prune/load/BabelFileFormats.java b/tim/prune/load/BabelFileFormats.java index ed7e627..2d1c9e8 100644 --- a/tim/prune/load/BabelFileFormats.java +++ b/tim/prune/load/BabelFileFormats.java @@ -3,7 +3,7 @@ package tim.prune.load; /** * Class to manage the list of file formats supported by Gpsbabel * (older versions of gpsbabel might not support all of these, of course). - * Certain supported formats such as txt, csv, gpx are not included here + * Certain supported formats such as txt, csv are not included here * as GpsPrune can already load them directly. */ public abstract class BabelFileFormats @@ -111,6 +111,7 @@ public abstract class BabelFileFormats "GPSman", "gpsman", null, "GPSPilot Tracker for Palm/OS", "gpspilot", null, "gpsutil", "gpsutil", null, + "GPX", "gpx", ".gpx", "HikeTech", "hiketech", null, "Holux (gm-100) .wpo Format", "holux", null, "Holux M-241 (MTK based) Binary File Format", "m241-bin", null, diff --git a/tim/prune/load/BabelLoadFromFile.java b/tim/prune/load/BabelLoadFromFile.java index f7866ef..1897e0c 100644 --- a/tim/prune/load/BabelLoadFromFile.java +++ b/tim/prune/load/BabelLoadFromFile.java @@ -25,6 +25,7 @@ import tim.prune.config.Config; import tim.prune.data.SourceInfo; import tim.prune.data.SourceInfo.FILE_TYPE; import tim.prune.gui.GuiGridLayout; +import tim.prune.load.babel.BabelFilterPanel; /** @@ -158,6 +159,15 @@ public class BabelLoadFromFile extends BabelLoader _saveCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT); mainPanel.add(_saveCheckbox); + // Filter panel + _filterPanel = new BabelFilterPanel(_parentFrame); + // Give filter panel the contents of the config + String filter = Config.getConfigString(Config.KEY_GPSBABEL_FILTER); + if (filter != null) { + _filterPanel.setFilterString(filter); + } + mainPanel.add(_filterPanel); + // progress bar (initially invisible) _progressBar = new JProgressBar(0, 10); mainPanel.add(_progressBar); @@ -216,6 +226,10 @@ public class BabelLoadFromFile extends BabelLoader */ protected void saveConfigValues() { - // nothing needed + // Save the filter string (but don't remove it if it's now blank) + final String filter = _filterPanel.getFilterString(); + if (filter != null && !filter.equals("")) { + Config.setConfigString(Config.KEY_GPSBABEL_FILTER, filter); + } } } diff --git a/tim/prune/load/BabelLoadFromGps.java b/tim/prune/load/BabelLoadFromGps.java index 8c4b4dd..130ca79 100644 --- a/tim/prune/load/BabelLoadFromGps.java +++ b/tim/prune/load/BabelLoadFromGps.java @@ -26,6 +26,7 @@ import tim.prune.I18nManager; import tim.prune.config.Config; import tim.prune.data.SourceInfo; import tim.prune.data.SourceInfo.FILE_TYPE; +import tim.prune.load.babel.BabelFilterPanel; /** * Class to manage the loading of data from a GPS device using GpsBabel @@ -128,6 +129,15 @@ public class BabelLoadFromGps extends BabelLoader _saveCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT); mainPanel.add(_saveCheckbox); + // Filter panel + _filterPanel = new BabelFilterPanel(_parentFrame); + // Give filter panel the contents of the config + String filter = Config.getConfigString(Config.KEY_GPSBABEL_FILTER); + if (filter != null) { + _filterPanel.setFilterString(filter); + } + mainPanel.add(_filterPanel); + // progress bar (initially invisible) _progressBar = new JProgressBar(0, 10); mainPanel.add(_progressBar); @@ -169,7 +179,9 @@ public class BabelLoadFromGps extends BabelLoader { final String device = _deviceField.getText().trim(); final String format = _formatField.getText().trim(); + final String filter = _filterPanel.getFilterString(); Config.setConfigString(Config.KEY_GPS_DEVICE, device); Config.setConfigString(Config.KEY_GPS_FORMAT, format); + Config.setConfigString(Config.KEY_GPSBABEL_FILTER, filter); } } diff --git a/tim/prune/load/BabelLoader.java b/tim/prune/load/BabelLoader.java index b970665..7ebd7de 100644 --- a/tim/prune/load/BabelLoader.java +++ b/tim/prune/load/BabelLoader.java @@ -20,8 +20,8 @@ import tim.prune.ExternalTools; import tim.prune.GenericFunction; import tim.prune.I18nManager; import tim.prune.config.Config; -import tim.prune.data.Altitude; import tim.prune.data.SourceInfo; +import tim.prune.load.babel.BabelFilterPanel; import tim.prune.load.xml.XmlFileLoader; import tim.prune.load.xml.XmlHandler; import tim.prune.save.GpxExporter; @@ -42,6 +42,7 @@ public abstract class BabelLoader extends GenericFunction implements Runnable protected JProgressBar _progressBar = null; protected File _saveFile = null; protected boolean _cancelled = false; + protected BabelFilterPanel _filterPanel = null; /** @@ -91,7 +92,11 @@ public abstract class BabelLoader extends GenericFunction implements Runnable /** Do any subclass-specific dialog initialisation necessary */ - protected void initDialog() {} + protected void initDialog() + { + // GPSBabel filter, if any + _filterPanel.setFilterString(Config.getConfigString(Config.KEY_GPSBABEL_FILTER)); + } /** * @param inStart true if the dialog is restarting @@ -229,9 +234,8 @@ public abstract class BabelLoader extends GenericFunction implements Runnable if (errorMessage.length() > 0) {throw new Exception(errorMessage);} // Send data back to app - _app.informDataLoaded(handler.getFieldArray(), handler.getDataArray(), Altitude.Format.METRES, - getSourceInfo(), - handler.getTrackNameList()); + _app.informDataLoaded(handler.getFieldArray(), handler.getDataArray(), null, + getSourceInfo(), handler.getTrackNameList()); } } @@ -242,33 +246,55 @@ public abstract class BabelLoader extends GenericFunction implements Runnable */ private String[] getCommandArray() { - String[] commands = null; + ArrayList commandList = new ArrayList(); + // Firstly the command for gpsbabel itself final String command = Config.getConfigString(Config.KEY_GPSBABEL_PATH); + commandList.add(command); + // Then whether to load waypoints or track points final boolean loadWaypoints = _waypointCheckbox.isSelected(); final boolean loadTrack = _trackCheckbox.isSelected(); - if (loadWaypoints && loadTrack) { - // Both waypoints and track points selected - commands = new String[] {command, "-w", "-t", "-i", getInputFormat(), - "-f", getFilePath(), "-o", "gpx", "-F", "-"}; + if (loadWaypoints) { + commandList.add("-w"); } - else + if (loadTrack) { + commandList.add("-t"); + } + // Input format + commandList.add("-i"); + commandList.add(getInputFormat()); + // File path + commandList.add("-f"); + commandList.add(getFilePath()); + // Filters, if any + final String filter = _filterPanel.getFilterString(); + if (filter != null && !filter.equals("")) { - // Only waypoints OR track points selected - commands = new String[] {command, "-w", "-i", getInputFormat(), - "-f", getFilePath(), "-o", "gpx", "-F", "-"}; - if (loadTrack) { - commands[1] = "-t"; + for (String arg : filter.split(" ")) + { + if (arg.length() > 0) { + commandList.add(arg); + } } } + // Output format + commandList.add("-o"); + commandList.add("gpx"); + // Where to + commandList.add("-F"); + String whereTo = "-"; // Do we want to save the gpx straight to file? - if (_saveCheckbox.isSelected()) { + if (_saveCheckbox.isSelected()) + { // Select file to save to _saveFile = GpxExporter.chooseGpxFile(_parentFrame); if (_saveFile != null) { - commands[commands.length-1] = _saveFile.getAbsolutePath(); + whereTo = _saveFile.getAbsolutePath(); } } - return commands; + commandList.add(whereTo); + // Convert to string array + String[] args = new String[] {}; + return commandList.toArray(args); } /** diff --git a/tim/prune/load/ComponentHider.java b/tim/prune/load/ComponentHider.java new file mode 100644 index 0000000..45eeccd --- /dev/null +++ b/tim/prune/load/ComponentHider.java @@ -0,0 +1,59 @@ +package tim.prune.load; + +import java.awt.Component; +import java.util.ArrayList; + +import tim.prune.data.Field; + +/** + * Class to hold a list of Components and fields, + * and then enable or disable them (setEnabled) according + * to whether those fields are available or not + */ +public class ComponentHider +{ + /** + * Inner class to hold each Component and its Field + */ + static class ComponentPair + { + public Component _component = null; + public Field _field = null; + /** Constructor */ + public ComponentPair(Component inComponent, Field inField) + { + _component = inComponent; + _field = inField; + } + } + + /** list itself */ + private ArrayList _componentList = new ArrayList(20); + + /** + * Add a new component to be controlled + * @param inComponent component to enable/disable + * @param inField associated field + */ + public void addComponent(Component inComponent, Field inField) + { + if (inComponent != null && inField != null) { + _componentList.add(new ComponentPair(inComponent, inField)); + } + } + + /** + * Enable or disable the components for the given field + * @param inField field + * @param inEnabled true for enabled, false for disabled + */ + public void enableComponents(Field inField, boolean inEnabled) + { + for (ComponentPair pair : _componentList) + { + if (pair != null && pair._field == inField) { + pair._component.setEnabled(inEnabled); + } + } + } +} diff --git a/tim/prune/load/FieldGuesser.java b/tim/prune/load/FieldGuesser.java index 9b2bdc7..c76318d 100644 --- a/tim/prune/load/FieldGuesser.java +++ b/tim/prune/load/FieldGuesser.java @@ -19,20 +19,42 @@ public abstract class FieldGuesser */ private static boolean isHeaderRow(String[] inValues) { - // Loop over values looking for a Latitude value + // Loop over values seeing if any are mostly numeric if (inValues != null) { - for (int v=0; v= '0' && currChar <= '9') {numNums++;} + } + // Return true if more than half the characters are numeric + return numNums > (numChars/2); + } + /** * Try to guess the fields for the given values from the file * @param inValues array of values from first non-blank line of file @@ -108,12 +130,24 @@ public abstract class FieldGuesser else if (!checkArrayHasField(fields, Field.LONGITUDE)) { fields[f] = Field.LONGITUDE; } - else { - customFieldNum++; - fields[f] = new Field(customPrefix + (customFieldNum)); + else + { + // Can we use the field name given? + Field customField = null; + if (isHeader && inValues[f] != null && inValues[f].length() > 0) { + customField = new Field(inValues[f]); + } + // Find an unused field number + while (customField == null || checkArrayHasField(fields, customField)) + { + customFieldNum++; + customField = new Field(customPrefix + (customFieldNum)); + } + fields[f] = customField; } } } + // Do a final check to make sure lat and long are in there if (!checkArrayHasField(fields, Field.LATITUDE)) { fields[0] = Field.LATITUDE; diff --git a/tim/prune/load/FileCacher.java b/tim/prune/load/FileCacher.java index 1d12397..da354aa 100644 --- a/tim/prune/load/FileCacher.java +++ b/tim/prune/load/FileCacher.java @@ -42,8 +42,14 @@ public class FileCacher { reader = new BufferedReader(new FileReader(_file)); String currLine = reader.readLine(); + if (currLine != null && currLine.startsWith("= 0) { + return; // it's a binary file, shouldn't use this cacher + } if (currLine.trim().length() > 0) contentList.add(currLine); currLine = reader.readLine(); diff --git a/tim/prune/load/FileLoader.java b/tim/prune/load/FileLoader.java index 5672c69..8669e18 100644 --- a/tim/prune/load/FileLoader.java +++ b/tim/prune/load/FileLoader.java @@ -2,11 +2,14 @@ package tim.prune.load; import java.io.File; import java.util.ArrayList; +import java.util.TreeSet; + import javax.swing.JFileChooser; import javax.swing.JFrame; import tim.prune.App; import tim.prune.config.Config; +import tim.prune.data.Photo; import tim.prune.load.xml.GzipFileLoader; import tim.prune.load.xml.XmlFileLoader; import tim.prune.load.xml.ZipFileLoader; @@ -130,6 +133,14 @@ public class FileLoader { _nmeaFileLoader.openFile(inFile); } + else if (fileExtension.equals(".jpg") || fileExtension.equals("jpeg")) + { + Photo photo = JpegLoader.createPhoto(inFile); + TreeSet photoSet = new TreeSet(); + photoSet.add(photo); + _app.informPhotosLoaded(photoSet); + _app.informNoDataLoaded(); // To trigger load of next file if any + } else { // Use text loader for everything else diff --git a/tim/prune/load/JpegLoader.java b/tim/prune/load/JpegLoader.java index 8a376d8..a177eaa 100644 --- a/tim/prune/load/JpegLoader.java +++ b/tim/prune/load/JpegLoader.java @@ -20,6 +20,7 @@ import tim.prune.data.Latitude; import tim.prune.data.Longitude; import tim.prune.data.Photo; import tim.prune.data.Timestamp; +import tim.prune.data.UnitSetLibrary; import tim.prune.function.Cancellable; import tim.prune.jpeg.ExifGateway; import tim.prune.jpeg.JpegData; @@ -318,7 +319,7 @@ public class JpegLoader implements Runnable, Cancellable Longitude longitude = new Longitude(lonval, Longitude.FORMAT_DEG_MIN_SEC); Altitude altitude = null; if (inData.hasAltitude()) { - altitude = new Altitude(inData.getAltitude(), Altitude.Format.METRES); + altitude = new Altitude(inData.getAltitude(), UnitSetLibrary.UNITS_METRES); } return new DataPoint(latitude, longitude, altitude); } diff --git a/tim/prune/load/MediaLoadProgressDialog.java b/tim/prune/load/MediaLoadProgressDialog.java index 4ed2ab0..a052b55 100644 --- a/tim/prune/load/MediaLoadProgressDialog.java +++ b/tim/prune/load/MediaLoadProgressDialog.java @@ -94,7 +94,6 @@ public class MediaLoadProgressDialog _progressBar.setMaximum(inMax); _progressBar.setValue(inCurrent); _progressBar.setString("" + inCurrent + " / " + _progressBar.getMaximum()); - // TODO: Need to repaint? } /** diff --git a/tim/prune/load/NmeaFileLoader.java b/tim/prune/load/NmeaFileLoader.java index 2dd63de..d62ba5f 100644 --- a/tim/prune/load/NmeaFileLoader.java +++ b/tim/prune/load/NmeaFileLoader.java @@ -7,7 +7,6 @@ import java.io.IOException; import java.util.ArrayList; import tim.prune.App; -import tim.prune.data.Altitude; import tim.prune.data.Field; import tim.prune.data.SourceInfo; @@ -92,8 +91,7 @@ public class NmeaFileLoader if (messages.size() > 0) { _app.informDataLoaded(getFieldArray(), makeDataArray(messages), - Altitude.Format.METRES, new SourceInfo(inFile, SourceInfo.FILE_TYPE.NMEA), - null); + null, new SourceInfo(inFile, SourceInfo.FILE_TYPE.NMEA), null); } } diff --git a/tim/prune/load/TextFileLoader.java b/tim/prune/load/TextFileLoader.java index 1f88012..53d6d2a 100644 --- a/tim/prune/load/TextFileLoader.java +++ b/tim/prune/load/TextFileLoader.java @@ -1,7 +1,6 @@ package tim.prune.load; import java.awt.BorderLayout; -import java.awt.CardLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; @@ -22,9 +21,13 @@ import java.io.File; import tim.prune.App; import tim.prune.I18nManager; -import tim.prune.data.Altitude; import tim.prune.data.Field; +import tim.prune.data.PointCreateOptions; import tim.prune.data.SourceInfo; +import tim.prune.data.Unit; +import tim.prune.data.UnitSetLibrary; +import tim.prune.gui.GuiGridLayout; +import tim.prune.gui.WizardLayout; /** @@ -37,8 +40,7 @@ public class TextFileLoader private App _app = null; private JFrame _parentFrame = null; private JDialog _dialog = null; - private JPanel _cardPanel = null; - private CardLayout _layout = null; + private WizardLayout _wizard = null; private JButton _backButton = null, _nextButton = null; private JButton _finishButton = null; private JButton _moveUpButton = null, _moveDownButton = null; @@ -51,14 +53,18 @@ public class TextFileLoader private FileExtractTableModel _fileExtractTableModel = null; private JTable _fieldTable; private FieldSelectionTableModel _fieldTableModel = null; - private JComboBox _unitsDropDown = null; + private JComboBox _altitudeUnitsDropdown = null; + private JComboBox _hSpeedUnitsDropdown = null; + private JComboBox _vSpeedUnitsDropdown = null; + private JRadioButton _vSpeedUpwardsRadio = null; + private ComponentHider _componentHider = null; private int _selectedField = -1; private char _currentDelimiter = ','; // previously selected values private char _lastUsedDelimiter = ','; private Field[] _lastSelectedFields = null; - private Altitude.Format _lastAltitudeFormat = Altitude.Format.NO_FORMAT; + private Unit _lastAltitudeUnit = null; // constants private static final int SNIPPET_SIZE = 6; @@ -135,9 +141,11 @@ public class TextFileLoader _dialog.pack(); _dialog.setVisible(true); } - else { + else + { // Didn't pass pre-check - _app.showErrorMessage("error.load.dialogtitle", "error.load.noread"); + _app.showErrorMessageNoLookup("error.load.dialogtitle", + I18nManager.getText("error.load.noread") + ": " + inFile.getName()); _app.informNoDataLoaded(); } } @@ -160,6 +168,9 @@ public class TextFileLoader // Check each line of the file String[] fileContents = _fileCacher.getContents(); + if (fileContents == null) { + return false; // nothing cached, might be binary + } boolean fileOK = true; _delimiterInfos = new DelimiterInfo[5]; for (int i=0; i<4; i++) _delimiterInfos[i] = new DelimiterInfo(DELIMITERS[i]); @@ -233,9 +244,9 @@ public class TextFileLoader _backButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - _layout.previous(_cardPanel); - _backButton.setEnabled(false); - _nextButton.setEnabled(true); + _wizard.showPreviousCard(); + _nextButton.setEnabled(!_wizard.isLastCard()); + _backButton.setEnabled(!_wizard.isFirstCard()); _finishButton.setEnabled(false); } }); @@ -245,11 +256,11 @@ public class TextFileLoader _nextButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - prepareSecondPanel(); - _layout.next(_cardPanel); - _nextButton.setEnabled(false); - _backButton.setEnabled(true); - _finishButton.setEnabled(_fieldTableModel.getRowCount() > 1); + prepareNextPanel(); // Maybe it needs to be initialized based on previous panels + _wizard.showNextCard(); + _nextButton.setEnabled(!_wizard.isLastCard() && isCurrentCardValid()); + _backButton.setEnabled(!_wizard.isFirstCard()); + _finishButton.setEnabled(_wizard.isLastCard() && isCurrentCardValid()); } }); buttonPanel.add(_nextButton); @@ -273,10 +284,9 @@ public class TextFileLoader 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); + // Make the card panel in the centre + JPanel cardPanel = new JPanel(); + _wizard = new WizardLayout(cardPanel); JPanel firstCard = new JPanel(); firstCard.setLayout(new BorderLayout()); firstCard.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15)); @@ -391,17 +401,71 @@ public class TextFileLoader innerPanel2.add(innerPanel3, BorderLayout.EAST); secondCard.add(innerPanel2, BorderLayout.CENTER); + + // Third card, for units selection of altitude and speeds + JPanel thirdCard = new JPanel(); + thirdCard.setLayout(new BorderLayout(10, 10)); + JPanel holderPanel = new JPanel(); + holderPanel.setLayout(new BoxLayout(holderPanel, BoxLayout.Y_AXIS)); + // Altitude 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); + GuiGridLayout altGrid = new GuiGridLayout(altUnitsPanel); + altUnitsPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("fieldname.altitude"))); + JLabel altLabel = new JLabel(I18nManager.getText("dialog.openoptions.altitudeunits") + ": "); + altGrid.add(altLabel); + String[] altUnits = {I18nManager.getText("units.metres"), I18nManager.getText("units.feet")}; + _altitudeUnitsDropdown = new JComboBox(altUnits); + altGrid.add(_altitudeUnitsDropdown); + holderPanel.add(altUnitsPanel); + // Horizontal speed + JPanel speedPanel = new JPanel(); + GuiGridLayout speedGrid = new GuiGridLayout(speedPanel); + speedPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("fieldname.speed"))); + JLabel speedLabel = new JLabel(I18nManager.getText("dialog.openoptions.speedunits") + ": "); + speedGrid.add(speedLabel); + _hSpeedUnitsDropdown = new JComboBox(); + for (Unit spUnit : UnitSetLibrary.ALL_SPEED_UNITS) { + _hSpeedUnitsDropdown.addItem(I18nManager.getText(spUnit.getNameKey())); + } + speedGrid.add(_hSpeedUnitsDropdown); + holderPanel.add(speedPanel); + // Vertical speed + JPanel vSpeedPanel = new JPanel(); + GuiGridLayout vSpeedGrid = new GuiGridLayout(vSpeedPanel); + vSpeedPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("fieldname.verticalspeed"))); + JLabel vSpeedLabel = new JLabel(I18nManager.getText("dialog.openoptions.vertspeedunits") + ": "); + vSpeedGrid.add(vSpeedLabel); + _vSpeedUnitsDropdown = new JComboBox(); + for (Unit spUnit : UnitSetLibrary.ALL_SPEED_UNITS) { + _vSpeedUnitsDropdown.addItem(I18nManager.getText(spUnit.getNameKey())); + } + vSpeedGrid.add(_vSpeedUnitsDropdown); + _vSpeedUpwardsRadio = new JRadioButton(I18nManager.getText("dialog.openoptions.vspeed.positiveup")); + JRadioButton vSpeedDownwardsRadio = new JRadioButton(I18nManager.getText("dialog.openoptions.vspeed.positivedown")); + ButtonGroup vSpeedDirGroup = new ButtonGroup(); + vSpeedDirGroup.add(_vSpeedUpwardsRadio); vSpeedDirGroup.add(vSpeedDownwardsRadio); + vSpeedGrid.add(_vSpeedUpwardsRadio); vSpeedGrid.add(vSpeedDownwardsRadio); + _vSpeedUpwardsRadio.setSelected(true); + holderPanel.add(vSpeedPanel); + thirdCard.add(holderPanel, BorderLayout.NORTH); + + // Make a hider to show and hide the components according to the selected fields + _componentHider = new ComponentHider(); + _componentHider.addComponent(altLabel, Field.ALTITUDE); + _componentHider.addComponent(_altitudeUnitsDropdown, Field.ALTITUDE); + _componentHider.addComponent(speedLabel, Field.SPEED); + _componentHider.addComponent(_hSpeedUnitsDropdown, Field.SPEED); + _componentHider.addComponent(vSpeedLabel, Field.VERTICAL_SPEED); + _componentHider.addComponent(_vSpeedUnitsDropdown, Field.VERTICAL_SPEED); + _componentHider.addComponent(_vSpeedUpwardsRadio, Field.VERTICAL_SPEED); + _componentHider.addComponent(vSpeedDownwardsRadio, Field.VERTICAL_SPEED); + + // Add cards to the wizard + _wizard.addCard(firstCard); + _wizard.addCard(secondCard); + _wizard.addCard(thirdCard); + + wholePanel.add(cardPanel, BorderLayout.CENTER); return wholePanel; } @@ -471,6 +535,26 @@ public class TextFileLoader } + /** + * Prepare the next panel to be shown, if necessary + */ + private void prepareNextPanel() + { + int currPanel = _wizard.getCurrentCardIndex(); + if (currPanel == 0) { + prepareSecondPanel(); + } + else if (currPanel == 1) + { + Field[] selectedFields = _fieldTableModel.getFieldArray(); + // Enable / disable controls based on whether altitude / speed / vspeed fields were chosen on second panel + _componentHider.enableComponents(Field.ALTITUDE, doesFieldArrayContain(selectedFields, Field.ALTITUDE)); + _componentHider.enableComponents(Field.SPEED, doesFieldArrayContain(selectedFields, Field.SPEED)); + _componentHider.enableComponents(Field.VERTICAL_SPEED, doesFieldArrayContain(selectedFields, Field.VERTICAL_SPEED)); + // TODO: Also check ranges of altitudes, speeds, vert speeds to show them in the third panel + } + } + /** * Use the delimiter selected to determine the fields in the file * and prepare the second panel accordingly @@ -512,14 +596,31 @@ public class TextFileLoader _fieldTable.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(fieldTypesBox)); // Set altitude format to same as last time if available - if (_lastAltitudeFormat == Altitude.Format.METRES) - _unitsDropDown.setSelectedIndex(0); - else if (_lastAltitudeFormat == Altitude.Format.FEET) - _unitsDropDown.setSelectedIndex(1); + if (_lastAltitudeUnit == UnitSetLibrary.UNITS_METRES) + _altitudeUnitsDropdown.setSelectedIndex(0); + else if (_lastAltitudeUnit == UnitSetLibrary.UNITS_FEET) + _altitudeUnitsDropdown.setSelectedIndex(1); // no selection on field list selectField(-1); } + /** + * See if the given array of selected fields contains the specified one + * @param inFields array of fields selected by user in the second panel + * @param inCheck field to check + * @return true if the field is present in the array + */ + private boolean doesFieldArrayContain(Field[] inFields, Field inCheck) + { + if (inFields != null) { + for (int i=0; i 1; + } + // all other panels are always valid + return true; + } /** * Make a panel with a label and a component diff --git a/tim/prune/load/babel/AddFilterDialog.java b/tim/prune/load/babel/AddFilterDialog.java new file mode 100644 index 0000000..7de9a3a --- /dev/null +++ b/tim/prune/load/babel/AddFilterDialog.java @@ -0,0 +1,185 @@ +package tim.prune.load.babel; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JPanel; + +import tim.prune.I18nManager; +import tim.prune.gui.WizardLayout; + + +/** + * Class to manage a dialog for adding a single GPSBabel filter + */ +public class AddFilterDialog +{ + /** Parent panel to pass the filter back to */ + private BabelFilterPanel _parentPanel = null; + /** Reference to parent frame */ + private JFrame _parentFrame = null; + /** Main dialog */ + private JDialog _dialog = null; + /** layout for dealing with cards */ + private WizardLayout _wizard = null; + /** Array of filter definitions */ + private FilterDefinition[] _filters = new FilterDefinition[4]; + /** Finish button */ + private JButton _finishButton = null; + /** back button */ + private JButton _backButton = null; + + // Selector class for one of the filter types + class FilterTypeListener implements ActionListener + { + private int _index = 0; + public FilterTypeListener(int inIndex) {_index = inIndex;} + public void actionPerformed(ActionEvent e) { + _wizard.showCard(_index); + _backButton.setEnabled(true); + filterParamsChanged(); // to check parameters and enable/disable Finish button + } + } + + /** + * Constructor + * @param inParent parent panel to inform of selected filter + * @param inParentFrame parent frame to reference for dialogs + */ + public AddFilterDialog(BabelFilterPanel inParent, JFrame inParentFrame) + { + _parentPanel = inParent; + _parentFrame = inParentFrame; + } + + /** + * Show the dialog to add a new filter + */ + public void showDialog() + { + if (_dialog == null) + { + _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.addfilter.title"), true); + _dialog.setLocationRelativeTo(_parentFrame); + _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + _dialog.getContentPane().add(makeDialogComponents()); + _dialog.pack(); + } + // TODO: Initialise cards, clear entries? + _wizard.showFirstCard(); + _backButton.setEnabled(false); + _finishButton.setEnabled(false); + _dialog.setVisible(true); + } + + /** + * Create dialog components + * @return Panel containing all gui elements in dialog + */ + private JPanel makeDialogComponents() + { + JPanel dialogPanel = new JPanel(); + dialogPanel.setLayout(new BorderLayout()); + + // card panel in the middle + JPanel cardPanel = new JPanel(); + _wizard = new WizardLayout(cardPanel); + JPanel typesCard = new JPanel(); + JButton discardButton = new JButton(I18nManager.getText("dialog.gpsbabel.filter.discard")); + discardButton.addActionListener(new FilterTypeListener(1)); + typesCard.add(discardButton); + JButton simplifyButton = new JButton(I18nManager.getText("dialog.gpsbabel.filter.simplify")); + simplifyButton.addActionListener(new FilterTypeListener(2)); + typesCard.add(simplifyButton); + JButton distanceButton = new JButton(I18nManager.getText("dialog.gpsbabel.filter.distance")); + distanceButton.addActionListener(new FilterTypeListener(3)); + typesCard.add(distanceButton); + JButton interpButton = new JButton(I18nManager.getText("dialog.gpsbabel.filter.interpolate")); + interpButton.addActionListener(new FilterTypeListener(4)); + typesCard.add(interpButton); + + // discard panel + _filters[0] = new DiscardFilter(this); + // simplify panel + _filters[1] = new SimplifyFilter(this); + // distance panel + _filters[2] = new DistanceFilter(this); + // interpolate panel + _filters[3] = new InterpolateFilter(this); + + // Add cards to the wizard + _wizard.addCard(typesCard); + _wizard.addCard(_filters[0]); + _wizard.addCard(_filters[1]); + _wizard.addCard(_filters[2]); + _wizard.addCard(_filters[3]); + dialogPanel.add(cardPanel, BorderLayout.CENTER); + + // button panel at bottom + 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) + { + _wizard.showCard(0); + _backButton.setEnabled(!_wizard.isFirstCard()); + _finishButton.setEnabled(false); + } + }); + _backButton.setEnabled(false); + buttonPanel.add(_backButton); + _finishButton = new JButton(I18nManager.getText("button.finish")); + ActionListener okListener = new ActionListener() { + public void actionPerformed(ActionEvent e) + { + finish(); + } + }; + _finishButton.addActionListener(okListener); + _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); + dialogPanel.add(buttonPanel, BorderLayout.SOUTH); + dialogPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15)); + + return dialogPanel; + } + + /** + * React to changes in the filter parameters (such as enabling/disabling the ok button) + */ + public void filterParamsChanged() + { + final int currCard = _wizard.getCurrentCardIndex(); + if (currCard > 0 && currCard < 5) { + _finishButton.setEnabled(_filters[currCard-1].isFilterValid()); + } + } + + /** + * Finish the dialog when OK pressed + */ + private void finish() + { + // finish dialog and pass results back to the parent panel + final int currCard = _wizard.getCurrentCardIndex(); + if (currCard > 0 && currCard < 5) { + _parentPanel.addFilter(_filters[currCard-1].getString()); + } + _dialog.dispose(); + } +} diff --git a/tim/prune/load/babel/BabelFilterPanel.java b/tim/prune/load/babel/BabelFilterPanel.java new file mode 100644 index 0000000..b5b564f --- /dev/null +++ b/tim/prune/load/babel/BabelFilterPanel.java @@ -0,0 +1,184 @@ +package tim.prune.load.babel; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.util.regex.Pattern; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; + +import tim.prune.I18nManager; +import tim.prune.gui.StatusIcon; + +/** + * Gui element to allow the specification of filters for GPSBabel. + * Used for loading from GPS and loading from file + */ +public class BabelFilterPanel extends JPanel +{ + /** Text field for entering filters manually */ + private JTextField _filterField = null; + /** Icon for showing whether the value is valid for GPSBabel or not */ + private StatusIcon _validIcon = null; + /** Dialog for adding a new filter */ + private AddFilterDialog _addDialog = null; + + /** Regular expression for detecting valid filter strings */ + private static final Pattern FILTER_PATTERN + = Pattern.compile("(-x [a-z,\\.0-9=]+ *)+"); + + /** + * Constructor + * @param inParentFrame parent frame for launching popup dialog + */ + public BabelFilterPanel(JFrame inParentFrame) + { + setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createTitledBorder(I18nManager.getText("dialog.gpsbabel.filters")), + BorderFactory.createEmptyBorder(2, 2, 2, 2))); + initPanel(); + _addDialog = new AddFilterDialog(this, inParentFrame); + } + + /** + * Set up the panel with all the components inside + */ + private void initPanel() + { + setLayout(new BorderLayout(4, 4)); + // text field for the filter text + _filterField = new JTextField(20); + _filterField.addKeyListener(new KeyAdapter() { + public void keyTyped(KeyEvent arg0) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + checkFilter(); + } + }); + } + }); + JPanel filterFieldPanel = new JPanel(); + filterFieldPanel.setLayout(new BorderLayout(3, 3)); + JPanel filterIconPanel = new JPanel(); + filterIconPanel.setLayout(new BorderLayout(3, 3)); + filterIconPanel.add(_filterField, BorderLayout.CENTER); + _validIcon = new StatusIcon(); + filterIconPanel.add(_validIcon, BorderLayout.EAST); + filterFieldPanel.add(filterIconPanel, BorderLayout.NORTH); + add(filterFieldPanel, BorderLayout.CENTER); + // Add and clear buttons + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.Y_AXIS)); + JButton addButton = new JButton(I18nManager.getText("button.addnew")); + addButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + // System.out.println("Filter exists: " + hasFilter() + ", valid: " + isFilterValid()); + _addDialog.showDialog(); + } + }); + buttonPanel.add(addButton); + buttonPanel.add(Box.createVerticalStrut(2)); + JButton clearButton = new JButton(I18nManager.getText("button.delete")); + clearButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + _filterField.setText(""); + checkFilter(); + } + }); + buttonPanel.add(clearButton); + add(buttonPanel, BorderLayout.EAST); + } + + /** + * @param inFilter filter string to set (normally from config) + */ + public void setFilterString(String inFilter) + { + if (inFilter != null && _filterField != null) { + _filterField.setText(inFilter.trim()); + } + checkFilter(); + } + + /** + * @return trimmed filter string, or null + */ + public String getFilterString() + { + String filter = _filterField.getText(); + if (filter != null) filter = filter.trim(); + return filter; + } + + /** + * @return true if a filter has been given (which may or may not be valid) + */ + public boolean hasFilter() + { + String str = getFilterString(); + return str != null && str.length() > 0; + } + + /** + * @return true if the given filter string is valid + */ + public boolean isFilterValid() + { + String str = getFilterString(); + if (str == null) return false; + return FILTER_PATTERN.matcher(str).matches(); + } + + /** + * Called from the add filter dialog to indicate completion + * @param inFilter filter to add + */ + public void addFilter(String inFilter) + { + if (inFilter != null) + { + String newFilter = inFilter.trim(); + String currFilter = getFilterString(); + if (!newFilter.equals("")) + { + if (currFilter == null || currFilter.equals("")) { + currFilter = newFilter; + } + else { // append + currFilter = currFilter + " " + newFilter; + } + } + _filterField.setText(currFilter); + } + checkFilter(); + } + + /** + * See if the current filter is valid or not, and update the icon accordingly + */ + private void checkFilter() + { + if (hasFilter()) + { + if (isFilterValid()) { + _validIcon.setStatusValid(); + } + else { + _validIcon.setStatusInvalid(); + } + } + else + { + _validIcon.setStatusBlank(); + } + } +} diff --git a/tim/prune/load/babel/DiscardFilter.java b/tim/prune/load/babel/DiscardFilter.java new file mode 100644 index 0000000..f310031 --- /dev/null +++ b/tim/prune/load/babel/DiscardFilter.java @@ -0,0 +1,144 @@ +package tim.prune.load.babel; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.GridLayout; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; +import javax.swing.border.EtchedBorder; + +import tim.prune.I18nManager; +import tim.prune.gui.WholeNumberField; + +/** + * Discard filter for GPSBabel + */ +public class DiscardFilter extends FilterDefinition +{ + /** Constructor */ + public DiscardFilter(AddFilterDialog inFilterDialog) + { + super(inFilterDialog); + makePanelContents(); + } + + private WholeNumberField _hdopField = null; + private WholeNumberField _vdopField = null; + private JComboBox _combineDopsCombo = null; + private WholeNumberField _numSatsField = null; + private JCheckBox _noFixCheckbox = null; + private JCheckBox _unknownFixCheckbox = null; + + + /** @return filter name */ + protected String getFilterName() { + return "discard"; + } + + /** Make the panel contents */ + protected void makePanelContents() + { + setLayout(new BorderLayout()); + JPanel boxPanel = new JPanel(); + boxPanel.setLayout(new BoxLayout(boxPanel, BoxLayout.Y_AXIS)); + JLabel topLabel = new JLabel(I18nManager.getText("dialog.gpsbabel.filter.discard.intro")); + topLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + boxPanel.add(topLabel); + boxPanel.add(Box.createVerticalStrut(9)); // spacer + + JPanel boxPanel2 = new JPanel(); + boxPanel2.setLayout(new BoxLayout(boxPanel2, BoxLayout.Y_AXIS)); + // Panel for dops + JPanel dopPanel = new JPanel(); + dopPanel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(4, 4, 4, 4)) + ); + dopPanel.setLayout(new GridLayout(0, 3, 4, 2)); + dopPanel.add(new JLabel(I18nManager.getText("dialog.gpsbabel.filter.discard.hdop"), SwingConstants.RIGHT)); + _hdopField = new WholeNumberField(2); + _hdopField.addKeyListener(_paramChangeListener); + dopPanel.add(_hdopField); + _combineDopsCombo = new JComboBox(new String[] {I18nManager.getText("logic.and"), I18nManager.getText("logic.or")}); + dopPanel.add(_combineDopsCombo); + dopPanel.add(new JLabel(I18nManager.getText("dialog.gpsbabel.filter.discard.vdop"), SwingConstants.RIGHT)); + _vdopField = new WholeNumberField(2); + _vdopField.addKeyListener(_paramChangeListener); + dopPanel.add(_vdopField); + boxPanel2.add(dopPanel); + + // Number of satellites + JPanel satPanel = new JPanel(); + satPanel.add(new JLabel(I18nManager.getText("dialog.gpsbabel.filter.discard.numsats"))); + _numSatsField = new WholeNumberField(2); + _numSatsField.addKeyListener(_paramChangeListener); + satPanel.add(_numSatsField); + boxPanel2.add(satPanel); + + // Checkboxes for no fix and unknown fix + _noFixCheckbox = new JCheckBox(I18nManager.getText("dialog.gpsbabel.filter.discard.nofix")); + boxPanel2.add(_noFixCheckbox); + _unknownFixCheckbox = new JCheckBox(I18nManager.getText("dialog.gpsbabel.filter.discard.unknownfix")); + boxPanel2.add(_unknownFixCheckbox); + boxPanel2.add(Box.createVerticalStrut(9)); // spacer + + boxPanel2.setAlignmentX(Component.LEFT_ALIGNMENT); + boxPanel.add(boxPanel2); + add(boxPanel, BorderLayout.NORTH); +} + + /** + * @return true if the filters are valid + */ + public boolean isFilterValid() + { + // If values are entered, insist that they're positive (0 not valid) + if (_hdopField.getText() != null && _hdopField.getText().length() > 0 && _hdopField.getValue() <= 0) {return false;} + if (_vdopField.getText() != null && _vdopField.getText().length() > 0 && _vdopField.getValue() <= 0) {return false;} + if (_numSatsField.getText() != null && _numSatsField.getText().length() > 0 && _numSatsField.getValue() <= 0) {return false;} + // Insist that at least one value has been entered + return _hdopField.getValue() > 0 || _vdopField.getValue() > 0 || _numSatsField.getValue() > 0; + } + + /** + * @return filter parameters as a string, or null + */ + protected String getParameters() + { + if (!isFilterValid()) return null; + StringBuilder builder = new StringBuilder(); + // hdop and vdop + final int hdop = _hdopField.getValue(); + if (hdop > 0) { + builder.append(",hdop=").append(hdop); + } + final int vdop = _vdopField.getValue(); + if (vdop > 0) + { + builder.append(",vdop=").append(vdop); + if (hdop > 0 && _combineDopsCombo.getSelectedIndex() == 0) { + builder.append(",hdopandvdop"); + } + } + // number of satellites + final int numSats = _numSatsField.getValue(); + if (numSats > 0) + { + builder.append(",sat=").append(numSats); + } + // checkboxes + if (_noFixCheckbox.isSelected()) { + builder.append(",fixnone"); + } + if (_unknownFixCheckbox.isSelected()) { + builder.append(",fixunknown"); + } + return builder.toString(); + } +} diff --git a/tim/prune/load/babel/DistanceFilter.java b/tim/prune/load/babel/DistanceFilter.java new file mode 100644 index 0000000..7554160 --- /dev/null +++ b/tim/prune/load/babel/DistanceFilter.java @@ -0,0 +1,104 @@ +package tim.prune.load.babel; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.GridLayout; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import tim.prune.I18nManager; +import tim.prune.gui.DecimalNumberField; +import tim.prune.gui.WholeNumberField; + +/** + * Distance filter for GPSBabel (compress by distance, or nearby points) + */ +public class DistanceFilter extends FilterDefinition +{ + /** Constructor */ + public DistanceFilter(AddFilterDialog inFilterDialog) + { + super(inFilterDialog); + makePanelContents(); + } + + private DecimalNumberField _distField = null; + private JComboBox _distUnitsCombo = null; + private WholeNumberField _secondsField = null; + + + /** @return filter name */ + protected String getFilterName() { + return "position"; + } + + /** Make the panel contents */ + protected void makePanelContents() + { + setLayout(new BorderLayout()); + JPanel boxPanel = new JPanel(); + boxPanel.setLayout(new BoxLayout(boxPanel, BoxLayout.Y_AXIS)); + add(boxPanel, BorderLayout.NORTH); + JLabel topLabel = new JLabel(I18nManager.getText("dialog.gpsbabel.filter.distance.intro")); + topLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + boxPanel.add(topLabel); + boxPanel.add(Box.createVerticalStrut(18)); // spacer + // Main three-column grid + JPanel gridPanel = new JPanel(); + gridPanel.setLayout(new GridLayout(0, 3, 4, 4)); + gridPanel.add(new JLabel(I18nManager.getText("dialog.gpsbabel.filter.distance.distance"))); + _distField = new DecimalNumberField(); + _distField.addKeyListener(_paramChangeListener); + gridPanel.add(_distField); + _distUnitsCombo = new JComboBox(new String[] {I18nManager.getText("units.metres"), I18nManager.getText("units.feet")}); + gridPanel.add(_distUnitsCombo); + gridPanel.add(new JLabel(I18nManager.getText("dialog.gpsbabel.filter.distance.time"))); + _secondsField = new WholeNumberField(4); + _secondsField.addKeyListener(_paramChangeListener); + gridPanel.add(_secondsField); + gridPanel.add(new JLabel(I18nManager.getText("units.seconds"))); + gridPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + boxPanel.add(gridPanel); + } + + /** + * @return true if the filters are valid + */ + public boolean isFilterValid() + { + if (_distField.getText() == null || _distField.getText().trim().equals("")) { + return false; // no distance given + } + final boolean timeGiven = _secondsField.getText() != null && _secondsField.getText().trim().length() > 0; + if (timeGiven && _secondsField.getValue() <= 1) { + return false; // must have a decent number of seconds + } + // check the distance + return (_distField.getValue() > 0.001); // no zero or negative distances allowed + } + + /** + * @return filter parameters as a string, or null + */ + protected String getParameters() + { + if (!isFilterValid()) return null; + StringBuilder builder = new StringBuilder(); + // Get the distance + double dValue = _distField.getValue(); + builder.append(",distance=").append(dValue); + // units of distance (miles by default) + builder.append(_distUnitsCombo.getSelectedIndex() == 0 ? "m" : "f"); // metres or feet + + // is there a time as well? + final boolean timeGiven = _secondsField.getText() != null && _secondsField.getText().trim().length() > 0; + if (timeGiven) { + builder.append(",time=").append(_secondsField.getValue()); // no s at the end + } + return builder.toString(); + } +} diff --git a/tim/prune/load/babel/FilterDefinition.java b/tim/prune/load/babel/FilterDefinition.java new file mode 100644 index 0000000..8417908 --- /dev/null +++ b/tim/prune/load/babel/FilterDefinition.java @@ -0,0 +1,59 @@ +package tim.prune.load.babel; + +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; + +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +/** + * Superclass of all the filter definition panels, to be added in the cardset + * of the AddFilterDialog + */ +public abstract class FilterDefinition extends JPanel +{ + /** Parent dialog to inform of parameter changes */ + private AddFilterDialog _parentDialog = null; + /** Listener for key presses on the parameter entry fields */ + protected KeyListener _paramChangeListener = null; + + /** + * Constructor + */ + public FilterDefinition(AddFilterDialog inFilterDialog) + { + _parentDialog = inFilterDialog; + _paramChangeListener = new KeyAdapter() { + public void keyTyped(KeyEvent arg0) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + _parentDialog.filterParamsChanged(); + } + }); + } + }; + } + + /** + * @return true if the filter definition is valid + */ + public abstract boolean isFilterValid(); + + /** + * @return filter definition to pass to gpsbabel + */ + public String getString() + { + return "-x " + getFilterName() + getParameters(); + } + + /** @return filter name */ + protected abstract String getFilterName(); + + /** Construct the GUI elements and add them to the panel */ + protected abstract void makePanelContents(); + + /** @return filter parameters */ + protected abstract String getParameters(); +} diff --git a/tim/prune/load/babel/InterpolateFilter.java b/tim/prune/load/babel/InterpolateFilter.java new file mode 100644 index 0000000..a169846 --- /dev/null +++ b/tim/prune/load/babel/InterpolateFilter.java @@ -0,0 +1,112 @@ +package tim.prune.load.babel; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.GridLayout; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import tim.prune.I18nManager; +import tim.prune.gui.DecimalNumberField; +import tim.prune.gui.WholeNumberField; + +/** + * Interpolate filter for GPSBabel (adding extra points, does that make it still a filter?) + * Very similar to the distance filter in terms of gui + */ +public class InterpolateFilter extends FilterDefinition +{ + /** Constructor */ + public InterpolateFilter(AddFilterDialog inFilterDialog) + { + super(inFilterDialog); + makePanelContents(); + } + + private DecimalNumberField _distField = null; + private JComboBox _distUnitsCombo = null; + private WholeNumberField _secondsField = null; + + + /** @return filter name */ + protected String getFilterName() { + return "interpolate"; + } + + /** Make the panel contents */ + protected void makePanelContents() + { + setLayout(new BorderLayout()); + JPanel boxPanel = new JPanel(); + boxPanel.setLayout(new BoxLayout(boxPanel, BoxLayout.Y_AXIS)); + add(boxPanel, BorderLayout.NORTH); + JLabel topLabel = new JLabel(I18nManager.getText("dialog.gpsbabel.filter.interpolate.intro")); + topLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + boxPanel.add(topLabel); + boxPanel.add(Box.createVerticalStrut(18)); // spacer + // Main three-column grid + JPanel gridPanel = new JPanel(); + gridPanel.setLayout(new GridLayout(0, 3, 4, 4)); + gridPanel.add(new JLabel(I18nManager.getText("dialog.gpsbabel.filter.interpolate.distance"))); + _distField = new DecimalNumberField(); + _distField.addKeyListener(_paramChangeListener); + gridPanel.add(_distField); + _distUnitsCombo = new JComboBox(new String[] {I18nManager.getText("units.kilometres"), I18nManager.getText("units.miles")}); + gridPanel.add(_distUnitsCombo); + gridPanel.add(new JLabel(I18nManager.getText("dialog.gpsbabel.filter.interpolate.time"))); + _secondsField = new WholeNumberField(4); + _secondsField.addKeyListener(_paramChangeListener); + gridPanel.add(_secondsField); + gridPanel.add(new JLabel(I18nManager.getText("units.seconds"))); + gridPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + boxPanel.add(gridPanel); + } + + /** + * @return true if the filters are valid + */ + public boolean isFilterValid() + { + final boolean distGiven = _distField.getText() != null && _distField.getText().trim().length() > 0; + final boolean timeGiven = _secondsField.getText() != null && _secondsField.getText().trim().length() > 0; + if ((!distGiven && !timeGiven) || (distGiven && timeGiven)) { + return false; // either one or the other, not both + } + if (distGiven && _distField.getValue() < 0.0001) { + return false; // must have a decent distance + } + if (timeGiven && _secondsField.getValue() <= 1) { + return false; // must have a decent number of seconds + } + // must be ok + return true; + } + + /** + * @return filter parameters as a string, or null + */ + protected String getParameters() + { + if (!isFilterValid()) return null; + StringBuilder builder = new StringBuilder(); + final boolean distGiven = _distField.getText() != null && _distField.getText().trim().length() > 0; + final boolean timeGiven = _secondsField.getText() != null && _secondsField.getText().trim().length() > 0; + if (distGiven) + { + // Get the distance + double dValue = _distField.getValue(); + builder.append(",distance=").append(dValue); + // units of distance (km or miles) + builder.append(_distUnitsCombo.getSelectedIndex() == 0 ? "k" : "m"); + } + else if (timeGiven) { + // time + builder.append(",time=").append(_secondsField.getValue()); // no s at the end + } + return builder.toString(); + } +} diff --git a/tim/prune/load/babel/SimplifyFilter.java b/tim/prune/load/babel/SimplifyFilter.java new file mode 100644 index 0000000..ebbc9f4 --- /dev/null +++ b/tim/prune/load/babel/SimplifyFilter.java @@ -0,0 +1,147 @@ +package tim.prune.load.babel; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.GridLayout; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; + +import tim.prune.I18nManager; +import tim.prune.data.UnitSetLibrary; +import tim.prune.gui.DecimalNumberField; +import tim.prune.gui.WholeNumberField; + +/** + * Simplify filter for GPSBabel + */ +public class SimplifyFilter extends FilterDefinition +{ + /** Constructor */ + public SimplifyFilter(AddFilterDialog inFilterDialog) + { + super(inFilterDialog); + makePanelContents(); + } + + private WholeNumberField _maxPointsField = null; + private DecimalNumberField _distField = null; + private JComboBox _distUnitsCombo = null; + private JRadioButton _crossTrackRadio = null; + private JRadioButton _lengthRadio = null; + private JRadioButton _relativeRadio = null; + + + /** @return filter name */ + protected String getFilterName() { + return "simplify"; + } + + /** Make the panel contents */ + protected void makePanelContents() + { + setLayout(new BorderLayout()); + JPanel boxPanel = new JPanel(); + boxPanel.setLayout(new BoxLayout(boxPanel, BoxLayout.Y_AXIS)); + add(boxPanel, BorderLayout.NORTH); + JLabel topLabel = new JLabel(I18nManager.getText("dialog.gpsbabel.filter.simplify.intro")); + topLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + boxPanel.add(topLabel); + boxPanel.add(Box.createVerticalStrut(18)); // spacer + // Main three-column grid + JPanel gridPanel = new JPanel(); + gridPanel.setLayout(new GridLayout(0, 3, 4, 4)); + gridPanel.add(new JLabel(I18nManager.getText("dialog.gpsbabel.filter.simplify.maxpoints"))); + _maxPointsField = new WholeNumberField(6); + _maxPointsField.addKeyListener(_paramChangeListener); + gridPanel.add(_maxPointsField); + gridPanel.add(new JLabel(" ")); + gridPanel.add(new JLabel(I18nManager.getText("dialog.gpsbabel.filter.simplify.maxerror"))); + _distField = new DecimalNumberField(); + _distField.addKeyListener(_paramChangeListener); + gridPanel.add(_distField); + _distUnitsCombo = new JComboBox(new String[] { + I18nManager.getText(UnitSetLibrary.UNITS_KILOMETRES.getNameKey()), + I18nManager.getText(UnitSetLibrary.UNITS_MILES.getNameKey()) + }); + gridPanel.add(_distUnitsCombo); + // radio buttons + _crossTrackRadio = new JRadioButton(I18nManager.getText("dialog.gpsbabel.filter.simplify.crosstrack")); + _crossTrackRadio.setSelected(true); + _lengthRadio = new JRadioButton(I18nManager.getText("dialog.gpsbabel.filter.simplify.length")); + _relativeRadio = new JRadioButton(I18nManager.getText("dialog.gpsbabel.filter.simplify.relative")); + ButtonGroup radioGroup = new ButtonGroup(); + radioGroup.add(_crossTrackRadio); + radioGroup.add(_lengthRadio); + radioGroup.add(_relativeRadio); + gridPanel.add(_crossTrackRadio); + gridPanel.add(_lengthRadio); + gridPanel.add(_relativeRadio); + gridPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + boxPanel.add(gridPanel); + } + + /** + * @return true if the filters are valid + */ + public boolean isFilterValid() + { + final boolean countGiven = _maxPointsField.getText() != null && _maxPointsField.getText().trim().length() > 0; + final boolean distGiven = _distField.getText() != null && _distField.getText().trim().length() > 0; + if ((!countGiven && !distGiven) || (countGiven && distGiven)) { + return false; // only one or the other allowed + } + if (countGiven && _maxPointsField.getValue() <= 1) { + return false; // must have a decent max points + } + if (distGiven && _distField.getValue() <= 0.001) { + return false; // no zero or negative distances allowed + } + // must be ok + return true; + } + + /** + * @return filter parameters as a string, or null + */ + protected String getParameters() + { + if (!isFilterValid()) return null; + StringBuilder builder = new StringBuilder(); + // type + final boolean countGiven = _maxPointsField.getText() != null && _maxPointsField.getText().trim().length() > 0; + final boolean distGiven = _distField.getText() != null && _distField.getText().trim().length() > 0; + if (countGiven) { + builder.append(",count=").append(_maxPointsField.getValue()); + } + else if (distGiven) + { + double dValue = 1.0; + try { + dValue = Double.parseDouble(_distField.getText()); + } + catch (Exception e) {} // shouldn't happen, otherwise validation would have failed + builder.append(",error=").append(dValue); + // units of distance (miles by default) + if (_distUnitsCombo.getSelectedIndex() == 0) { + builder.append("k"); // nothing for miles + } + } + // three options + if (_crossTrackRadio.isSelected()) { + builder.append(",crosstrack"); // default, could not pass it + } + else if (_lengthRadio.isSelected()) { + builder.append(",length"); + } + else if (_relativeRadio.isSelected()) { + builder.append(",relative"); + } + return builder.toString(); + } +} diff --git a/tim/prune/load/xml/GzipFileLoader.java b/tim/prune/load/xml/GzipFileLoader.java index 98ef946..3ebd4ca 100644 --- a/tim/prune/load/xml/GzipFileLoader.java +++ b/tim/prune/load/xml/GzipFileLoader.java @@ -7,7 +7,6 @@ import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import tim.prune.App; import tim.prune.I18nManager; -import tim.prune.data.Altitude; import tim.prune.data.SourceInfo; import tim.prune.load.MediaLinkInfo; @@ -55,7 +54,7 @@ public class GzipFileLoader SourceInfo sourceInfo = new SourceInfo(inFile, (handler instanceof GpxHandler?SourceInfo.FILE_TYPE.GPX:SourceInfo.FILE_TYPE.KML)); _app.informDataLoaded(handler.getFieldArray(), handler.getDataArray(), - Altitude.Format.METRES, sourceInfo, handler.getTrackNameList(), + null, sourceInfo, handler.getTrackNameList(), new MediaLinkInfo(inFile, handler.getLinkArray())); } } diff --git a/tim/prune/load/xml/XmlFileLoader.java b/tim/prune/load/xml/XmlFileLoader.java index 4c4239d..783ac79 100644 --- a/tim/prune/load/xml/XmlFileLoader.java +++ b/tim/prune/load/xml/XmlFileLoader.java @@ -10,9 +10,9 @@ import javax.xml.parsers.SAXParserFactory; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; + import tim.prune.App; import tim.prune.I18nManager; -import tim.prune.data.Altitude; import tim.prune.data.SourceInfo; import tim.prune.load.MediaLinkInfo; @@ -86,7 +86,7 @@ public class XmlFileLoader extends DefaultHandler implements Runnable SourceInfo sourceInfo = new SourceInfo(_file, (_handler instanceof GpxHandler?SourceInfo.FILE_TYPE.GPX:SourceInfo.FILE_TYPE.KML)); _app.informDataLoaded(_handler.getFieldArray(), _handler.getDataArray(), - Altitude.Format.METRES, sourceInfo, _handler.getTrackNameList(), + null, sourceInfo, _handler.getTrackNameList(), new MediaLinkInfo(_handler.getLinkArray())); } } diff --git a/tim/prune/load/xml/ZipFileLoader.java b/tim/prune/load/xml/ZipFileLoader.java index fff0d92..74e48f9 100644 --- a/tim/prune/load/xml/ZipFileLoader.java +++ b/tim/prune/load/xml/ZipFileLoader.java @@ -11,7 +11,6 @@ import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import tim.prune.App; -import tim.prune.data.Altitude; import tim.prune.data.SourceInfo; import tim.prune.load.MediaLinkInfo; @@ -69,7 +68,7 @@ public class ZipFileLoader SourceInfo sourceInfo = new SourceInfo(inFile, (handler instanceof GpxHandler?SourceInfo.FILE_TYPE.GPX:SourceInfo.FILE_TYPE.KML)); _app.informDataLoaded(handler.getFieldArray(), handler.getDataArray(), - Altitude.Format.METRES, sourceInfo, handler.getTrackNameList(), + null, sourceInfo, handler.getTrackNameList(), new MediaLinkInfo(inFile, handler.getLinkArray())); xmlFound = true; } @@ -117,7 +116,7 @@ public class ZipFileLoader { // Send back to app _app.informDataLoaded(handler.getFieldArray(), handler.getDataArray(), - Altitude.Format.METRES, new SourceInfo("gpsies", SourceInfo.FILE_TYPE.GPSIES), + new SourceInfo("gpsies", SourceInfo.FILE_TYPE.GPSIES), handler.getTrackNameList()); xmlFound = true; } diff --git a/tim/prune/readme.txt b/tim/prune/readme.txt index 6cddea5..f5379c3 100644 --- a/tim/prune/readme.txt +++ b/tim/prune/readme.txt @@ -1,5 +1,5 @@ -GpsPrune version 14.1 -===================== +GpsPrune version 15 +=================== GpsPrune is an application for viewing, editing and managing coordinate data from GPS systems, including format conversion, charting and photo correlation. @@ -17,7 +17,7 @@ Running ======= To run GpsPrune from the jar file, simply call it from a command prompt or shell: - java -jar gpsprune_14.1.jar + java -jar gpsprune_15.jar If the jar file is saved in a different directory, you will need to include the path. Depending on your system settings, you may be able to click or double-click on the jar file @@ -25,17 +25,25 @@ in a file manager window to execute it. A shortcut, menu item, alias, desktop i or other link can of course be made should you wish. To specify a language other than the default, use an additional parameter, eg: - java -jar gpsprune_14.1.jar --lang=DE + java -jar gpsprune_15.jar --lang=DE -New with version 14.1 +New with version 15 ===================== The following features were added since version 14: - - Addition and correction of translations - - Correction of version number in build scripts + - Extend povray output using map image on base plane + - Export an image of the map and track at a selected zoom level + - Estimation of hiking times and learining of parameter values + - Allow altitude / speed profile to show any arbitrary field + - Accept files dragged and dropped onto the GpsPrune window + - Take account of timezone if present in track timestamps + - Allow timestamp exports in KML using gx extensions + - GPSBabel filters + - Improved wikipedia name lookup + - Allow loading of speeds and vertical speeds from text files New with version 14 -=================== +===================== The following features were added since version 13: - Dragging of existing points - Creation of new points by dragging the halfway point between two points diff --git a/tim/prune/save/BaseImageConfigDialog.java b/tim/prune/save/BaseImageConfigDialog.java new file mode 100644 index 0000000..514ebd7 --- /dev/null +++ b/tim/prune/save/BaseImageConfigDialog.java @@ -0,0 +1,495 @@ +package tim.prune.save; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.io.File; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import tim.prune.DataSubscriber; +import tim.prune.I18nManager; +import tim.prune.config.Config; +import tim.prune.data.Track; +import tim.prune.gui.map.MapSource; +import tim.prune.gui.map.MapSourceLibrary; + +/** + * Dialog to let you choose the parameters for a base image + * (source and zoom) + */ +public class BaseImageConfigDialog implements Runnable +{ + /** Parent to notify */ + private DataSubscriber _parent = null; + /** Parent dialog for position */ + private JDialog _parentDialog = null; + /** Track to use for preview image */ + private Track _track = null; + /** Dialog to show */ + private JDialog _dialog = null; + /** Checkbox for using an image or not */ + private JCheckBox _useImageCheckbox = null; + /** Panel to hold the other controls */ + private JPanel _mainPanel = null; + /** Dropdown for map source */ + private JComboBox _mapSourceDropdown = null; + /** Dropdown for zoom levels */ + private JComboBox _zoomDropdown = null; + /** Warning label that image is incomplete */ + private JLabel _imageIncompleteLabel = null; + /** Label for number of tiles found */ + private JLabel _tilesFoundLabel = null; + /** Label for image size in pixels */ + private JLabel _imageSizeLabel = null; + /** Image preview panel */ + private ImagePreviewPanel _previewPanel = null; + /** OK button, needs to be enabled/disabled */ + private JButton _okButton = null; + /** Flag for rebuilding dialog, don't bother refreshing and recalculating */ + private boolean _rebuilding = false; + /** Cached values to allow cancellation of dialog */ + private boolean _useImage = false; + private int _sourceIndex = 0; + private int _zoomLevel = 0; + + + /** + * Constructor + * @param inParent parent object to notify on completion of dialog + * @param inParentDialog parent dialog + * @param inTrack track object + */ + public BaseImageConfigDialog(DataSubscriber inParent, JDialog inParentDialog, Track inTrack) + { + _parent = inParent; + _parentDialog = inParentDialog; + _dialog = new JDialog(inParentDialog, I18nManager.getText("dialog.baseimage.title"), true); + _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + _dialog.getContentPane().add(makeDialogComponents()); + _dialog.pack(); + _track = inTrack; + } + + /** + * Begin the function + */ + public void begin() + { + initDialog(); + _dialog.setLocationRelativeTo(_parentDialog); + _dialog.setVisible(true); + } + + /** + * Begin the function with a default of using an image + */ + public void beginWithImageYes() + { + initDialog(); + _useImageCheckbox.setSelected(true); + refreshDialog(); + _dialog.setVisible(true); + } + + /** + * Initialise the dialog from the cached values + */ + private void initDialog() + { + _rebuilding = true; + _useImageCheckbox.setSelected(_useImage); + // Populate the dropdown of map sources from the library in case it has changed + _mapSourceDropdown.removeAllItems(); + for (int i=0; i= _mapSourceDropdown.getItemCount()) { + _sourceIndex = 0; + } + _mapSourceDropdown.setSelectedIndex(_sourceIndex); + + // Zoom level + if (_useImage) + { + for (int i=0; i<_zoomDropdown.getItemCount(); i++) + { + String item = _zoomDropdown.getItemAt(i).toString(); + try { + if (Integer.parseInt(item) == _zoomLevel) { + _zoomDropdown.setSelectedIndex(i); + break; + } + } + catch (NumberFormatException nfe) {} + } + } + _rebuilding = false; + refreshDialog(); + } + + /** + * Update the visibility of the controls, and update the zoom dropdown based on the selected map source + */ + private void refreshDialog() + { + _mainPanel.setVisible(_useImageCheckbox.isSelected()); + // Exit if we're in the middle of something + if (_rebuilding) {return;} + int currentZoom = 0; + try { + currentZoom = Integer.parseInt(_zoomDropdown.getSelectedItem().toString()); + } + catch (Exception nfe) {} + // Get the extent of the track so we can work out how big the images are going to be for each zoom level + // System.out.println("Ranges are: x=" + _track.getXRange().getRange() + ", y=" + _track.getYRange().getRange()); + final double xyExtent = Math.max(_track.getXRange().getRange(), _track.getYRange().getRange()); + int zoomToSelect = -1; + + _rebuilding = true; + _zoomDropdown.removeAllItems(); + if (_useImageCheckbox.isSelected() && _mapSourceDropdown.getItemCount() > 0) + { + int currentSource = _mapSourceDropdown.getSelectedIndex(); + for (int i=5; i<18; i++) + { + // How many pixels does this give? + final int zoomFactor = 1 << i; + final int pixCount = (int) (xyExtent * zoomFactor * 256); + if (pixCount > 100 // less than this isn't worth it + && pixCount < 4000 // don't want to run out of memory + && isZoomAvailable(i, MapSourceLibrary.getSource(currentSource))) + { + _zoomDropdown.addItem("" + i); + if (i == currentZoom) { + zoomToSelect = _zoomDropdown.getItemCount() - 1; + } + } + // else System.out.println("Not using zoom " + i + " because pixCount=" + pixCount + " and xyExtent=" + xyExtent); + } + } + _zoomDropdown.setSelectedIndex(zoomToSelect); + _rebuilding = false; + + _okButton.setEnabled(!_useImageCheckbox.isSelected() || + (_zoomDropdown.getItemCount() > 0 && _zoomDropdown.getSelectedIndex() >= 0)); + updateImagePreview(); + } + + /** + * @return true if it should be possible to use an image, false if no disk cache or cache empty + */ + public static boolean isImagePossible() + { + String path = Config.getConfigString(Config.KEY_DISK_CACHE); + if (path != null && !path.equals("")) + { + File cacheDir = new File(path); + if (cacheDir.exists() && cacheDir.isDirectory()) + { + // Check if there are any directories in the cache + for (File subdir : cacheDir.listFiles()) + { + if (subdir.exists() && subdir.isDirectory()) { + return true; + } + } + } + } + return false; + } + + /** + * See if the requested zoom level is available + * @param inZoom zoom level + * @param inSource selected map source + * @return true if there is a zoom directory for each of the source's layers + */ + private static boolean isZoomAvailable(int inZoom, MapSource inSource) + { + if (inSource == null) {return false;} + String path = Config.getConfigString(Config.KEY_DISK_CACHE); + if (path == null || path.equals("")) { + return false; + } + File cacheDir = new File(path); + if (!cacheDir.exists() || !cacheDir.isDirectory()) { + return false; + } + // First layer + File layer0 = new File(cacheDir, inSource.getSiteName(0) + inZoom); + if (!layer0.exists() || !layer0.isDirectory() || !layer0.canRead()) { + return false; + } + // Second layer, if any + if (inSource.getNumLayers() > 1) + { + File layer1 = new File(cacheDir, inSource.getSiteName(1) + inZoom); + if (!layer1.exists() || !layer1.isDirectory() || !layer1.canRead()) { + return false; + } + } + // must be ok + return true; + } + + + /** + * @return true if image has been selected + */ + public boolean useImage() { + return _useImage; + } + + /** + * @return index of selected image source + */ + public int getSourceIndex() { + return _sourceIndex; + } + + /** + * @return selected zoom level + */ + public int getZoomLevel() { + return _zoomLevel; + } + + /** + * Make the dialog components to select the options + * @return Component holding gui elements + */ + private Component makeDialogComponents() + { + JPanel panel = new JPanel(); + panel.setLayout(new BorderLayout()); + _useImageCheckbox = new JCheckBox(I18nManager.getText("dialog.baseimage.useimage")); + _useImageCheckbox.setBorder(BorderFactory.createEmptyBorder(4, 4, 6, 4)); + _useImageCheckbox.setHorizontalAlignment(JLabel.CENTER); + _useImageCheckbox.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + refreshDialog(); + } + }); + panel.add(_useImageCheckbox, BorderLayout.NORTH); + + // Outer panel with the grid and the map preview + _mainPanel = new JPanel(); + _mainPanel.setLayout(new BorderLayout(1, 10)); + // Central stuff with labels and dropdowns + JPanel controlsPanel = new JPanel(); + controlsPanel.setLayout(new GridLayout(0, 2, 10, 4)); + // map source + JLabel sourceLabel = new JLabel(I18nManager.getText("dialog.baseimage.mapsource") + ": "); + sourceLabel.setHorizontalAlignment(JLabel.RIGHT); + controlsPanel.add(sourceLabel); + _mapSourceDropdown = new JComboBox(); + _mapSourceDropdown.addItem("name of map source"); + // Add listener to dropdown to change zoom levels + _mapSourceDropdown.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + refreshDialog(); + } + }); + controlsPanel.add(_mapSourceDropdown); + // zoom level + JLabel zoomLabel = new JLabel(I18nManager.getText("dialog.baseimage.zoom") + ": "); + zoomLabel.setHorizontalAlignment(JLabel.RIGHT); + controlsPanel.add(zoomLabel); + _zoomDropdown = new JComboBox(); + // Add action listener to enable ok button when zoom changed + _zoomDropdown.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + if (_zoomDropdown.getSelectedIndex() >= 0) { + _okButton.setEnabled(true); + updateImagePreview(); + } + } + }); + controlsPanel.add(_zoomDropdown); + _mainPanel.add(controlsPanel, BorderLayout.NORTH); + + JPanel imagePanel = new JPanel(); + imagePanel.setLayout(new BorderLayout(10, 1)); + // image preview + _previewPanel = new ImagePreviewPanel(); + imagePanel.add(_previewPanel, BorderLayout.CENTER); + + // Label panel on right + JPanel labelPanel = new JPanel(); + labelPanel.setLayout(new BorderLayout()); + _imageIncompleteLabel = new JLabel(I18nManager.getText("dialog.baseimage.incomplete")); + _imageIncompleteLabel.setForeground(Color.RED); + _imageIncompleteLabel.setVisible(false); + labelPanel.add(_imageIncompleteLabel, BorderLayout.NORTH); + JPanel labelGridPanel = new JPanel(); + labelGridPanel.setLayout(new GridLayout(0, 2, 10, 4)); + labelGridPanel.add(new JLabel(I18nManager.getText("dialog.baseimage.tiles") + ": ")); + _tilesFoundLabel = new JLabel("11 / 11"); + labelGridPanel.add(_tilesFoundLabel); + labelGridPanel.add(new JLabel(I18nManager.getText("dialog.baseimage.size") + ": ")); + _imageSizeLabel = new JLabel("1430"); + labelGridPanel.add(_imageSizeLabel); + labelGridPanel.add(new JLabel(" ")); // just for spacing + labelPanel.add(labelGridPanel, BorderLayout.SOUTH); + imagePanel.add(labelPanel, BorderLayout.EAST); + + _mainPanel.add(imagePanel, BorderLayout.CENTER); + panel.add(_mainPanel, BorderLayout.CENTER); + + // OK, Cancel buttons + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + _okButton = new JButton(I18nManager.getText("button.ok")); + _okButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + // Check values, maybe don't want to exit + if (!_useImageCheckbox.isSelected() + || (_mapSourceDropdown.getSelectedIndex() >= 0 && _zoomDropdown.getSelectedIndex() >= 0)) + { + storeValues(); + _dialog.dispose(); + } + } + }); + buttonPanel.add(_okButton); + JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _dialog.dispose(); + } + }); + buttonPanel.add(cancelButton); + panel.add(buttonPanel, BorderLayout.SOUTH); + + // Listener to close dialog if escape pressed + KeyAdapter closer = new KeyAdapter() { + public void keyReleased(KeyEvent e) + { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + _dialog.dispose(); + } + } + }; + _useImageCheckbox.addKeyListener(closer); + _mapSourceDropdown.addKeyListener(closer); + _zoomDropdown.addKeyListener(closer); + _okButton.addKeyListener(closer); + cancelButton.addKeyListener(closer); + + return panel; + } + + /** + * Use the selected settings to make a preview image and (asynchronously) update the preview panel + */ + private void updateImagePreview() + { + // Clear labels + _imageIncompleteLabel.setVisible(false); + _tilesFoundLabel.setText(""); + _imageSizeLabel.setText(""); + if (_useImageCheckbox.isSelected() && _mapSourceDropdown.getSelectedIndex() >= 0 + && _zoomDropdown.getItemCount() > 0 && _zoomDropdown.getSelectedIndex() >= 0) + { + _previewPanel.startLoading(); + // Launch a separate thread to create an image and pass it to the preview panel + new Thread(this).start(); + } + else { + // clear preview + _previewPanel.setImage(null); + } + } + + /** + * Store the selected details in the variables + */ + private void storeValues() + { + // Store values of controls in variables + _useImage = _useImageCheckbox.isSelected(); + _sourceIndex = _mapSourceDropdown.getSelectedIndex(); + try { + String zoomStr = _zoomDropdown.getSelectedItem().toString(); + _zoomLevel = Integer.parseInt(zoomStr); + } + catch (Exception nfe) { + _zoomLevel = 0; + } + // Call parent to retrieve values + _parent.dataUpdated(DataSubscriber.ALL); + } + + /** + * Run method for separate thread. Uses the current dialog parameters + * to trigger a call to the Grouter, and pass the image to the preview panel + */ + public void run() + { + // Remember the current dropdown indices, so we know whether they've changed or not + final int mapIndex = _mapSourceDropdown.getSelectedIndex(); + final int zoomIndex = _zoomDropdown.getSelectedIndex(); + if (!_useImageCheckbox.isSelected() || mapIndex < 0 || zoomIndex < 0) {return;} + + // Get the map source and zoom level + MapSource mapSource = MapSourceLibrary.getSource(mapIndex); + int zoomLevel = 0; + try { + zoomLevel = Integer.parseInt(_zoomDropdown.getSelectedItem().toString()); + } + catch (Exception e) {} + + // Use the Grouter to create an image (slow, blocks thread) + GroutedImage groutedImage = MapGrouter.createMapImage(_track, mapSource, zoomLevel); + + // If the dialog hasn't changed, pass the generated image to the preview panel + if (_useImageCheckbox.isSelected() + && _mapSourceDropdown.getSelectedIndex() == mapIndex + && _zoomDropdown.getSelectedIndex() == zoomIndex + && groutedImage != null) + { + _previewPanel.setImage(groutedImage); + // Set values of labels + _imageIncompleteLabel.setVisible(!groutedImage.isComplete()); + _tilesFoundLabel.setText(groutedImage.getNumTilesUsed() + " / " + groutedImage.getNumTilesTotal()); + if (groutedImage.getImageSize() > 0) { + _imageSizeLabel.setText("" + groutedImage.getImageSize()); + } + else { + _imageSizeLabel.setText(""); + } + } + else + { + _previewPanel.setImage(null); + // Clear labels + _imageIncompleteLabel.setVisible(false); + _tilesFoundLabel.setText(""); + _imageSizeLabel.setText(""); + } + } + + /** + * @return true if any map data has been found for the image + */ + public boolean getFoundData() + { + return _useImage && _zoomLevel > 0 && _previewPanel != null && _previewPanel.getTilesFound(); + } +} diff --git a/tim/prune/save/ExifSaver.java b/tim/prune/save/ExifSaver.java index 020ef53..3c1a7b8 100644 --- a/tim/prune/save/ExifSaver.java +++ b/tim/prune/save/ExifSaver.java @@ -24,7 +24,6 @@ import tim.prune.ExternalTools; import tim.prune.I18nManager; import tim.prune.UpdateMessageBroker; import tim.prune.config.Config; -import tim.prune.data.Altitude; import tim.prune.data.Coordinate; import tim.prune.data.DataPoint; import tim.prune.data.Photo; @@ -244,7 +243,7 @@ public class ExifSaver implements Runnable { // Only look at photos which are selected and whose status has changed since load photo = entry.getPhoto(); - if (photo != null && photo.getOriginalStatus() != photo.getCurrentStatus()) + if (photo != null && photo.isModified()) { // Increment counter if save successful if (savePhoto(photo, overwriteFlag, false)) { @@ -415,7 +414,7 @@ public class ExifSaver implements Runnable result[paramOffset + 3] = "-GPSLongitudeRef=" + inPoint.getLongitude().output(Coordinate.FORMAT_CARDINAL); // add altitude if it has it result[paramOffset + 4] = "-GPSAltitude=" - + (inPoint.hasAltitude()?inPoint.getAltitude().getValue(Altitude.Format.METRES):0); + + (inPoint.hasAltitude()?inPoint.getAltitude().getMetricValue():0); result[paramOffset + 5] = "-GPSAltitudeRef='Above Sea Level'"; // add the filename to modify result[paramOffset + 6] = inFile.getAbsolutePath(); diff --git a/tim/prune/save/FileSaver.java b/tim/prune/save/FileSaver.java index 6be44eb..b4a73d4 100644 --- a/tim/prune/save/FileSaver.java +++ b/tim/prune/save/FileSaver.java @@ -35,7 +35,6 @@ import tim.prune.App; import tim.prune.I18nManager; import tim.prune.UpdateMessageBroker; import tim.prune.config.Config; -import tim.prune.data.Altitude; import tim.prune.data.Coordinate; import tim.prune.data.DataPoint; import tim.prune.data.Field; @@ -43,6 +42,8 @@ import tim.prune.data.FieldList; import tim.prune.data.RecentFile; import tim.prune.data.Timestamp; import tim.prune.data.Track; +import tim.prune.data.Unit; +import tim.prune.data.UnitSetLibrary; import tim.prune.load.GenericFileFilter; import tim.prune.load.OneCharDocument; @@ -72,7 +73,7 @@ public class FileSaver private static final int[] FORMAT_COORDS = {Coordinate.FORMAT_NONE, Coordinate.FORMAT_DEG_MIN_SEC, Coordinate.FORMAT_DEG_MIN, Coordinate.FORMAT_DEG}; - private static final Altitude.Format[] FORMAT_ALTS = {Altitude.Format.NO_FORMAT, Altitude.Format.METRES, Altitude.Format.FEET}; + private static final Unit[] UNIT_ALTS = {null, UnitSetLibrary.UNITS_METRES, UnitSetLibrary.UNITS_FEET}; private static final int[] FORMAT_TIMES = {Timestamp.FORMAT_ORIGINAL, Timestamp.FORMAT_LOCALE, Timestamp.FORMAT_ISO_8601}; @@ -428,11 +429,11 @@ public class FileSaver for (int i=0; i<_coordUnitsRadios.length; i++) if (_coordUnitsRadios[i].isSelected()) coordFormat = FORMAT_COORDS[i]; - Altitude.Format altitudeFormat = Altitude.Format.NO_FORMAT; + Unit altitudeUnit = null; for (int i=0; i<_altitudeUnitsRadios.length; i++) { if (_altitudeUnitsRadios[i].isSelected()) { - altitudeFormat = FORMAT_ALTS[i]; + altitudeUnit = UNIT_ALTS[i]; } } // Get timestamp format @@ -518,7 +519,7 @@ public class FileSaver if (!firstField) { buffer.append(delimiter); } - saveField(buffer, point, info.getField(), coordFormat, altitudeFormat, timestampFormat); + saveField(buffer, point, info.getField(), coordFormat, altitudeUnit, timestampFormat); firstField = false; } } @@ -567,11 +568,11 @@ public class FileSaver * @param inPoint point object * @param inField field object * @param inCoordFormat coordinate format - * @param inAltitudeFormat altitude format + * @param inAltitudeUnit altitude unit * @param inTimestampFormat timestamp format */ private void saveField(StringBuffer inBuffer, DataPoint inPoint, Field inField, - int inCoordFormat, Altitude.Format inAltitudeFormat, int inTimestampFormat) + int inCoordFormat, Unit inAltitudeUnit, int inTimestampFormat) { // Output field according to type if (inField == Field.LATITUDE) @@ -586,7 +587,7 @@ public class FileSaver { try { - inBuffer.append(inPoint.getAltitude().getStringValue(inAltitudeFormat)); + inBuffer.append(inPoint.getAltitude().getStringValue(inAltitudeUnit)); } catch (NullPointerException npe) {} } diff --git a/tim/prune/save/GpxExporter.java b/tim/prune/save/GpxExporter.java index 19007ac..8d9df8a 100644 --- a/tim/prune/save/GpxExporter.java +++ b/tim/prune/save/GpxExporter.java @@ -35,7 +35,6 @@ import tim.prune.GpsPrune; import tim.prune.I18nManager; import tim.prune.UpdateMessageBroker; import tim.prune.config.Config; -import tim.prune.data.Altitude; import tim.prune.data.AudioClip; import tim.prune.data.Coordinate; import tim.prune.data.DataPoint; @@ -45,6 +44,7 @@ import tim.prune.data.Photo; import tim.prune.data.RecentFile; import tim.prune.data.Timestamp; import tim.prune.data.TrackInfo; +import tim.prune.data.UnitSetLibrary; import tim.prune.gui.DialogCloser; import tim.prune.load.GenericFileFilter; import tim.prune.save.xml.GpxCacherList; @@ -530,7 +530,7 @@ public class GpxExporter extends GenericFunction implements Runnable // Point has been modified - maybe it's possible to modify the source source = replaceGpxTags(source, "lat=\"", "\"", inPoint.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT)); source = replaceGpxTags(source, "lon=\"", "\"", inPoint.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT)); - source = replaceGpxTags(source, "", "", inPoint.getAltitude().getStringValue(Altitude.Format.METRES)); + source = replaceGpxTags(source, "", "", inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES)); source = replaceGpxTags(source, "", inPoint.getTimestamp().getText(Timestamp.FORMAT_ISO_8601)); if (inPoint.isWaypoint()) { @@ -726,7 +726,7 @@ public class GpxExporter extends GenericFunction implements Runnable if (inPoint.hasAltitude()) { inWriter.write("\t\t"); - inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES)); + inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES)); inWriter.write("\n"); } // timestamp if available (point might have timestamp and then be turned into a waypoint) @@ -798,7 +798,7 @@ public class GpxExporter extends GenericFunction implements Runnable if (inPoint.hasAltitude()) { inWriter.write(""); - inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES)); + inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES)); inWriter.write(""); } // timestamp if available (and selected) diff --git a/tim/prune/save/GroutedImage.java b/tim/prune/save/GroutedImage.java new file mode 100644 index 0000000..4400067 --- /dev/null +++ b/tim/prune/save/GroutedImage.java @@ -0,0 +1,111 @@ +package tim.prune.save; + +import java.awt.image.BufferedImage; + +import tim.prune.data.DoubleRange; + + +/** + * Class to represent the result of the MapGrouter's assembly of map tiles + * into a single image. Includes information about how complete the result is. + */ +public class GroutedImage +{ + private BufferedImage _image = null; + private int _numTilesFound = 0; + private int _numTilesMissing = 0; + private DoubleRange _xRange = null; + private DoubleRange _yRange = null; + + /** + * Constructor + * @param inImage image, or null if no image possible + * @param inTilesUsed number of tiles used + * @param inTilesMissing number of tiles which could not be found + */ + public GroutedImage(BufferedImage inImage, int inTilesUsed, int inTilesMissing) + { + _image = inImage; + _numTilesFound = inTilesUsed; + _numTilesMissing = inTilesMissing; + } + + /** + * @return true if any content at all was found + */ + public boolean isValid() { + return _image != null && _numTilesFound > 0; + } + + /** + * @return true if all the required tiles were found + */ + public boolean isComplete() { + return _numTilesMissing == 0; + } + + /** + * @return the pixel dimensions of the result image + */ + public int getImageSize() + { + if (_image == null) {return -1;} + return _image.getWidth(); + } + + /** + * @return the image object + */ + public BufferedImage getImage() { + return _image; + } + + /** + * @return the number of tiles used in the image + */ + public int getNumTilesUsed() { + return _numTilesFound; + } + + /** + * @return the number of tiles which could not be found, leaving gaps in the image + */ + public int getNumTilesMissing() { + return _numTilesMissing; + } + + /** + * @return the total number of tiles + */ + public int getNumTilesTotal() { + return _numTilesFound + _numTilesMissing; + } + + /** + * @param inRange x range of data + */ + public void setXRange(DoubleRange inRange) { + _xRange = inRange; + } + + /** + * @return x range of data + */ + public DoubleRange getXRange() { + return _xRange; + } + + /** + * @param inRange y range of data + */ + public void setYRange(DoubleRange inRange) { + _yRange = inRange; + } + + /** + * @return y range of data + */ + public DoubleRange getYRange() { + return _yRange; + } +} diff --git a/tim/prune/save/ImageExporter.java b/tim/prune/save/ImageExporter.java new file mode 100644 index 0000000..fabed33 --- /dev/null +++ b/tim/prune/save/ImageExporter.java @@ -0,0 +1,454 @@ +package tim.prune.save; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.border.EtchedBorder; + +import tim.prune.App; +import tim.prune.DataSubscriber; +import tim.prune.GenericFunction; +import tim.prune.I18nManager; +import tim.prune.config.ColourScheme; +import tim.prune.config.Config; +import tim.prune.data.DataPoint; +import tim.prune.data.DoubleRange; +import tim.prune.data.Track; +import tim.prune.gui.GuiGridLayout; +import tim.prune.gui.WholeNumberField; +import tim.prune.gui.map.MapSource; +import tim.prune.gui.map.MapSourceLibrary; +import tim.prune.gui.map.MapUtils; +import tim.prune.load.GenericFileFilter; + +/** + * Class to handle the exporting of map images, optionally with track data drawn on top. + * This allows images larger than the screen to be generated. + */ +public class ImageExporter extends GenericFunction implements DataSubscriber +{ + private JDialog _dialog = null; + private JCheckBox _drawDataCheckbox = null; + private WholeNumberField _textScaleField = null; + private JLabel _baseImageLabel = null; + private BaseImageConfigDialog _baseImageConfig = null; + private JFileChooser _fileChooser = null; + private JButton _okButton = null; + + /** + * Constructor + * @param inApp App object + */ + public ImageExporter(App inApp) + { + super(inApp); + } + + /** Get the name key */ + public String getNameKey() { + return "function.exportimage"; + } + + /** + * Begin the function by showing the input dialog + */ + public void begin() + { + // Make dialog window + if (_dialog == null) + { + _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true); + _dialog.setLocationRelativeTo(_parentFrame); + _dialog.getContentPane().add(makeDialogComponents()); + _dialog.pack(); + _textScaleField.setValue(100); + } + // Make base image dialog too + if (_baseImageConfig == null) { + _baseImageConfig = new BaseImageConfigDialog(this, _dialog, _app.getTrackInfo().getTrack()); + } + + // Check if there is a cache to use + if (!BaseImageConfigDialog.isImagePossible()) + { + _app.showErrorMessage(getNameKey(), "dialog.exportimage.noimagepossible"); + return; + } + + updateBaseImageDetails(); + // Show dialog + _dialog.setVisible(true); + } + + /** + * Make the dialog components to select the export options + * @return Component holding gui elements + */ + private Component makeDialogComponents() + { + JPanel panel = new JPanel(); + panel.setLayout(new BorderLayout(4, 4)); + // Checkbox for drawing track or not + _drawDataCheckbox = new JCheckBox(I18nManager.getText("dialog.exportimage.drawtrack")); + _drawDataCheckbox.setSelected(true); // draw by default + + // TODO: Maybe have other controls such as line width, symbol scale factor + JPanel controlsPanel = new JPanel(); + GuiGridLayout grid = new GuiGridLayout(controlsPanel); + grid.add(new JLabel(I18nManager.getText("dialog.exportimage.textscalepercent") + ": ")); + _textScaleField = new WholeNumberField(3); + _textScaleField.setText("888"); + grid.add(_textScaleField); + + // OK, Cancel buttons + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + _okButton = new JButton(I18nManager.getText("button.ok")); + _okButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + doExport(); + MapGrouter.clearMapImage(); + _dialog.dispose(); + } + }); + buttonPanel.add(_okButton); + JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + MapGrouter.clearMapImage(); + _dialog.dispose(); + } + }); + buttonPanel.add(cancelButton); + panel.add(buttonPanel, BorderLayout.SOUTH); + + // Listener to close dialog if escape pressed + KeyAdapter closer = new KeyAdapter() { + public void keyReleased(KeyEvent e) + { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + _dialog.dispose(); + MapGrouter.clearMapImage(); + } + } + }; + _drawDataCheckbox.addKeyListener(closer); + + // Panel for the base image + JPanel imagePanel = new JPanel(); + imagePanel.setLayout(new BorderLayout(10, 4)); + imagePanel.add(new JLabel(I18nManager.getText("dialog.exportpov.baseimage") + ": "), BorderLayout.WEST); + _baseImageLabel = new JLabel("Typical sourcename"); + imagePanel.add(_baseImageLabel, BorderLayout.CENTER); + JButton baseImageButton = new JButton(I18nManager.getText("button.edit")); + baseImageButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent event) { + changeBaseImage(); + } + }); + baseImageButton.addKeyListener(closer); + imagePanel.add(baseImageButton, BorderLayout.EAST); + imagePanel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(4, 4, 4, 4)) + ); + + // add these panels to the holder panel + JPanel holderPanel = new JPanel(); + holderPanel.setLayout(new BorderLayout(5, 5)); + holderPanel.add(_drawDataCheckbox, BorderLayout.NORTH); + holderPanel.add(controlsPanel, BorderLayout.CENTER); + holderPanel.add(imagePanel, BorderLayout.SOUTH); + holderPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); + + panel.add(holderPanel, BorderLayout.NORTH); + return panel; + } + + /** + * Change the base image by calling the BaseImageConfigDialog + */ + private void changeBaseImage() + { + // Check if there is a cache to use + if (BaseImageConfigDialog.isImagePossible()) + { + // Show new dialog to choose image details + _baseImageConfig.beginWithImageYes(); + } + } + + /** + * Callback from base image config dialog + */ + public void dataUpdated(byte inUpdateType) + { + updateBaseImageDetails(); + } + + /** Not required */ + public void actionCompleted(String inMessage) { + } + + /** + * Update the description label according to the selected base image details + */ + private void updateBaseImageDetails() + { + String desc = null; + if (_baseImageConfig.useImage()) + { + MapSource source = MapSourceLibrary.getSource(_baseImageConfig.getSourceIndex()); + if (source != null) { + desc = source.getName() + " (" + + _baseImageConfig.getZoomLevel() + ")"; + } + } + if (desc == null) { + desc = I18nManager.getText("dialog.about.no"); + } + _baseImageLabel.setText(desc); + _okButton.setEnabled(_baseImageConfig.useImage() && _baseImageConfig.getFoundData() + && MapGrouter.isZoomLevelOk(_app.getTrackInfo().getTrack(), _baseImageConfig.getZoomLevel())); + } + + /** + * Select the file and export data to it + */ + private void doExport() + { + // OK pressed, so choose output file + _okButton.setEnabled(false); + if (_fileChooser == null) + { + _fileChooser = new JFileChooser(); + _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG); + _fileChooser.setFileFilter(new GenericFileFilter("filetype.png", new String[] {"png"})); + _fileChooser.setAcceptAllFileFilterUsed(false); + // start from directory in config which should be set + final String configDir = Config.getConfigString(Config.KEY_TRACK_DIR); + if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));} + } + + // Allow choose again if an existing file is selected + boolean chooseAgain = false; + do + { + chooseAgain = false; + if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION) + { + // OK pressed and file chosen + File pngFile = _fileChooser.getSelectedFile(); + if (!pngFile.getName().toLowerCase().endsWith(".png")) + { + pngFile = new File(pngFile.getAbsolutePath() + ".png"); + } + // Check if file exists and if necessary prompt for overwrite + Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")}; + if (!pngFile.exists() || JOptionPane.showOptionDialog(_parentFrame, + I18nManager.getText("dialog.save.overwrite.text"), + I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1]) + == JOptionPane.YES_OPTION) + { + // Export the file + if (!exportFile(pngFile)) + { + // export failed so need to choose again + chooseAgain = true; + } + } + else + { + // overwrite cancelled so need to choose again + chooseAgain = true; + } + } + } while (chooseAgain); + } + + /** + * Export the track data to the specified file + * @param inPngFile File object to save to + * @return true if successful + */ + private boolean exportFile(File inPngFile) + { + // Get the image file from the grouter + MapSource source = MapSourceLibrary.getSource(_baseImageConfig.getSourceIndex()); + GroutedImage baseImage = MapGrouter.getMapImage(_app.getTrackInfo().getTrack(), source, + _baseImageConfig.getZoomLevel()); + if (baseImage == null || !baseImage.isValid()) + { + _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage"); + return true; + } + try + { + if (_drawDataCheckbox.isSelected()) + { + // Draw the track on top of this image + drawData(baseImage); + } + // Write composite image to file + if (!ImageIO.write(baseImage.getImage(), "png", inPngFile)) { + _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage"); + return false; // choose again - the image creation worked but the save failed + } + } + catch (IOException ioe) { + System.err.println("Can't write image: " + ioe.getClass().getName()); + } + return true; + } + + /** + * Draw the track and waypoint data from the current Track onto the given image + * @param inImage GroutedImage from map tiles + */ + private void drawData(GroutedImage inImage) + { + // Work out x, y limits for drawing + DoubleRange xRange = inImage.getXRange(); + DoubleRange yRange = inImage.getYRange(); + int zoomFactor = 1 << _baseImageConfig.getZoomLevel(); + Graphics g = inImage.getImage().getGraphics(); + // TODO: Set colour, line width + g.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_POINT)); + + // Loop over points + final Track track = _app.getTrackInfo().getTrack(); + final int numPoints = track.getNumPoints(); + int prevX = 0, prevY = 0; + for (int i=0; i 1) + { + if (_baseImage.isValid()) + { + inG.drawImage(_baseImage.getImage(), + (width-previewSize)/2, (height-previewSize)/2, previewSize, previewSize, this); + } + else + { + // No content found at all + inG.setColor(EMPTY_IMAGE_COLOUR); + inG.fillRect((width-previewSize)/2, (height-previewSize)/2, previewSize, previewSize); + } + // draw frame around it to make it more obvious + inG.setColor(Color.BLACK); + inG.drawRect((width-previewSize)/2, (height-previewSize)/2, previewSize-1, previewSize-1); + } + } + } + + /** + * @return true if there is an image to use and it contains map tiles + */ + public boolean getTilesFound() + { + return _baseImage != null && _baseImage.isValid() && _baseImage.getImageSize() > 1 + && _baseImage.getNumTilesUsed() > 0; + } +} diff --git a/tim/prune/save/KmlExporter.java b/tim/prune/save/KmlExporter.java index e1cc151..6fbf749 100644 --- a/tim/prune/save/KmlExporter.java +++ b/tim/prune/save/KmlExporter.java @@ -23,6 +23,7 @@ import javax.imageio.ImageIO; import javax.imageio.ImageWriter; import javax.swing.Box; import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBox; @@ -32,6 +33,7 @@ import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JProgressBar; +import javax.swing.JRadioButton; import javax.swing.JTextField; import javax.swing.SwingConstants; @@ -41,17 +43,19 @@ import tim.prune.I18nManager; import tim.prune.UpdateMessageBroker; import tim.prune.config.ColourUtils; import tim.prune.config.Config; -import tim.prune.data.Altitude; import tim.prune.data.Coordinate; import tim.prune.data.DataPoint; import tim.prune.data.Field; import tim.prune.data.RecentFile; +import tim.prune.data.Timestamp; import tim.prune.data.Track; import tim.prune.data.TrackInfo; +import tim.prune.data.UnitSetLibrary; import tim.prune.gui.ColourChooser; import tim.prune.gui.ColourPatch; import tim.prune.gui.DialogCloser; import tim.prune.gui.ImageUtils; +import tim.prune.gui.WholeNumberField; import tim.prune.load.GenericFileFilter; import tim.prune.save.xml.XmlUtils; @@ -66,9 +70,12 @@ public class KmlExporter extends GenericFunction implements Runnable private JDialog _dialog = null; private JTextField _descriptionField = null; private PointTypeSelector _pointTypeSelector = null; + private JRadioButton _gxExtensionsRadio = null; private JCheckBox _altitudesCheckbox = null; private JCheckBox _kmzCheckbox = null; private JCheckBox _exportImagesCheckbox = null; + private JLabel _imageSizeLabel = null; + private WholeNumberField _imageSizeField = null; private ColourPatch _colourPatch = null; private JLabel _progressLabel = null; private JProgressBar _progressBar = null; @@ -83,7 +90,6 @@ public class KmlExporter extends GenericFunction implements Runnable private static final String KML_FILENAME_IN_KMZ = "doc.kml"; // Default width and height of thumbnail images in Kmz private static final int DEFAULT_THUMBNAIL_WIDTH = 240; - private static final int DEFAULT_THUMBNAIL_HEIGHT = 240; // Default track colour private static final Color DEFAULT_TRACK_COLOUR = new Color(204, 0, 0); // red @@ -119,6 +125,8 @@ public class KmlExporter extends GenericFunction implements Runnable _dialog.pack(); _colourChooser = new ColourChooser(_dialog); } + // Fill in image size from config + _imageSizeField.setValue(Config.getConfigInt(Config.KEY_KMZ_IMAGE_SIZE)); enableCheckboxes(); _descriptionField.setEnabled(true); _okButton.setEnabled(true); @@ -169,19 +177,31 @@ public class KmlExporter extends GenericFunction implements Runnable colourPanel.add(new JLabel(I18nManager.getText("dialog.exportkml.trackcolour"))); colourPanel.add(_colourPatch); mainPanel.add(colourPanel); + // Pair of radio buttons for standard/extended KML + JRadioButton standardKmlRadio = new JRadioButton(I18nManager.getText("dialog.exportkml.standardkml")); + _gxExtensionsRadio = new JRadioButton(I18nManager.getText("dialog.exportkml.extendedkml")); + ButtonGroup bGroup = new ButtonGroup(); + bGroup.add(standardKmlRadio); bGroup.add(_gxExtensionsRadio); + JPanel radioPanel = new JPanel(); + radioPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 1)); + radioPanel.add(standardKmlRadio); + radioPanel.add(_gxExtensionsRadio); + standardKmlRadio.setSelected(true); + mainPanel.add(radioPanel); // Checkbox for altitude export _altitudesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.altitude")); _altitudesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT); _altitudesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT); mainPanel.add(_altitudesCheckbox); + // Checkboxes for kmz export and image export _kmzCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.kmz")); _kmzCheckbox.setHorizontalTextPosition(SwingConstants.LEFT); _kmzCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT); + // enable image checkbox if kmz activated _kmzCheckbox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { - // enable image checkbox if kmz activated enableCheckboxes(); } }); @@ -189,7 +209,23 @@ public class KmlExporter extends GenericFunction implements Runnable _exportImagesCheckbox = new JCheckBox(I18nManager.getText("dialog.exportkml.exportimages")); _exportImagesCheckbox.setHorizontalTextPosition(SwingConstants.LEFT); _exportImagesCheckbox.setAlignmentX(Component.CENTER_ALIGNMENT); + // enable image size fields if image checkbox changes + _exportImagesCheckbox.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent arg0) { + enableImageSizeFields(); + } + }); mainPanel.add(_exportImagesCheckbox); + // Panel for the image size + JPanel imageSizePanel = new JPanel(); + imageSizePanel.setLayout(new FlowLayout(FlowLayout.CENTER)); + _imageSizeLabel = new JLabel(I18nManager.getText("dialog.exportkml.imagesize")); + _imageSizeLabel.setAlignmentX(Component.RIGHT_ALIGNMENT); + imageSizePanel.add(_imageSizeLabel); + _imageSizeField = new WholeNumberField(4); + imageSizePanel.add(_imageSizeField); + mainPanel.add(imageSizePanel); + mainPanel.add(Box.createVerticalStrut(10)); _progressLabel = new JLabel("..."); _progressLabel.setAlignmentX(Component.CENTER_ALIGNMENT); @@ -237,9 +273,26 @@ public class KmlExporter extends GenericFunction implements Runnable boolean hasPhotos = _trackInfo.getPhotoList() != null && _trackInfo.getPhotoList().getNumPhotos() > 0; _exportImagesCheckbox.setSelected(hasPhotos && _kmzCheckbox.isSelected()); _exportImagesCheckbox.setEnabled(hasPhotos && _kmzCheckbox.isSelected()); + enableImageSizeFields(); + } + + /** + * Enable and disable the image size fields according to the checkboxes + */ + private void enableImageSizeFields() + { + boolean exportImages = _exportImagesCheckbox.isEnabled() && _exportImagesCheckbox.isSelected(); + _imageSizeField.setEnabled(exportImages); + _imageSizeLabel.setEnabled(exportImages); } + /** + * @return true if using gx extensions for kml export + */ + private boolean useGxExtensions() { + return _gxExtensionsRadio.isSelected(); + } /** * Start the export process based on the input parameters */ @@ -325,11 +378,6 @@ public class KmlExporter extends GenericFunction implements Runnable boolean exportImages = exportToKmz && _exportImagesCheckbox.isSelected(); _progressBar.setMaximum(exportImages?getNumPhotosToExport():1); - // Determine photo thumbnail size from config - int thumbWidth = Config.getConfigInt(Config.KEY_KMZ_IMAGE_WIDTH); - if (thumbWidth < DEFAULT_THUMBNAIL_WIDTH) {thumbWidth = DEFAULT_THUMBNAIL_WIDTH;} - int thumbHeight = Config.getConfigInt(Config.KEY_KMZ_IMAGE_HEIGHT); - if (thumbHeight < DEFAULT_THUMBNAIL_HEIGHT) {thumbHeight = DEFAULT_THUMBNAIL_HEIGHT;} // Create array for image dimensions in case it's required _imageDimensions = new Dimension[_track.getNumPoints()]; @@ -350,9 +398,14 @@ public class KmlExporter extends GenericFunction implements Runnable // Export images into zip file too if requested if (exportImages) { + // Get entered value for image size, store in config + int thumbSize = _imageSizeField.getValue(); + if (thumbSize < DEFAULT_THUMBNAIL_WIDTH) {thumbSize = DEFAULT_THUMBNAIL_WIDTH;} + Config.setConfigInt(Config.KEY_KMZ_IMAGE_SIZE, thumbSize); + // Create thumbnails of each photo in turn and add to zip as images/image.jpg // This is done first so that photo sizes are known for later - exportThumbnails(zipOutputStream, thumbWidth, thumbHeight); + exportThumbnails(zipOutputStream, thumbSize); } writer = new OutputStreamWriter(zipOutputStream); // Make an entry in the zip file for the kml file @@ -420,8 +473,14 @@ public class KmlExporter extends GenericFunction implements Runnable boolean writePhotos = _pointTypeSelector.getPhotopointsSelected(); boolean writeAudios = _pointTypeSelector.getAudiopointsSelected(); boolean justSelection = _pointTypeSelector.getJustSelection(); - inWriter.write("\n\n\n"); - inWriter.write("\t"); + // Define xml header (depending on whether extensions are used or not) + if (useGxExtensions()) { + inWriter.write("\n\n"); + } + else { + inWriter.write("\n\n"); + } + inWriter.write("\n\t"); if (_descriptionField != null && _descriptionField.getText() != null && !_descriptionField.getText().equals("")) { inWriter.write(_descriptionField.getText()); @@ -492,51 +551,168 @@ public class KmlExporter extends GenericFunction implements Runnable // Make a line for the track, if there is one if (hasTrackpoints && writeTrack) { - // Set up strings for start and end of track segment - String trackStart = "\t\n\t\ttrack\n\t\t\n\t\t\n"; - if (absoluteAltitudes) { - trackStart += "\t\t\t1\n\t\t\tabsolute\n"; + boolean useGxExtensions = _gxExtensionsRadio.isSelected(); + if (useGxExtensions) + { + // Write track using the Google Extensions to KML including gx:Track + numSaved += writeGxTrack(inWriter, absoluteAltitudes, selStart, selEnd); } else { - trackStart += "\t\t\tclampToGround\n"; + // Write track using standard KML + numSaved += writeStandardTrack(inWriter, absoluteAltitudes, selStart, selEnd); } - trackStart += "\t\t\t"; - String trackEnd = "\t\t\t\n\t\t\n\t"; - - // Start segment - inWriter.write(trackStart); - // Loop over track points - boolean firstTrackpoint = true; - for (i=0; i\n"); + return numSaved; + } + + + /** + * Write out the track using standard KML LineString tag + * @param inWriter writer object to write to + * @param inAbsoluteAltitudes true to use absolute altitudes, false to clamp to ground + * @param inSelStart start index of selection, or -1 if whole track + * @param inSelEnd end index of selection, or -1 if whole track + * @return number of track points written + */ + private int writeStandardTrack(OutputStreamWriter inWriter, boolean inAbsoluteAltitudes, int inSelStart, + int inSelEnd) + throws IOException + { + int numSaved = 0; + // Set up strings for start and end of track segment + String trackStart = "\t\n\t\ttrack\n\t\t\n\t\t\n"; + if (inAbsoluteAltitudes) { + trackStart += "\t\t\t1\n\t\t\tabsolute\n"; + } + else { + trackStart += "\t\t\tclampToGround\n"; + } + trackStart += "\t\t\t"; + String trackEnd = "\t\t\t\n\t\t\n\t"; + + boolean justSelection = _pointTypeSelector.getJustSelection(); + + // Start segment + inWriter.write(trackStart); + // Loop over track points + boolean firstTrackpoint = true; + final int numPoints = _track.getNumPoints(); + for (int i=0; i=inSelStart && i<=inSelEnd); + if (!point.isWaypoint() && writeCurrentPoint) { - point = _track.getPoint(i); - boolean writeCurrentPoint = !justSelection || (i>=selStart && i<=selEnd); - if (!point.isWaypoint() && writeCurrentPoint) + // start new track segment if necessary + if (point.getSegmentStart() && !firstTrackpoint) { + inWriter.write(trackEnd); + inWriter.write(trackStart); + } + if (point.getPhoto() == null) { - // start new track segment if necessary - if (point.getSegmentStart() && !firstTrackpoint) { - inWriter.write(trackEnd); - inWriter.write(trackStart); + exportTrackpoint(point, inWriter); + numSaved++; + firstTrackpoint = false; + } + } + } + // end segment + inWriter.write(trackEnd); + return numSaved; + } + + + /** + * Write out the track using Google's KML Extensions such as gx:Track + * @param inWriter writer object to write to + * @param inAbsoluteAltitudes true to use absolute altitudes, false to clamp to ground + * @param inSelStart start index of selection, or -1 if whole track + * @param inSelEnd end index of selection, or -1 if whole track + * @return number of track points written + */ + private int writeGxTrack(OutputStreamWriter inWriter, boolean inAbsoluteAltitudes, int inSelStart, + int inSelEnd) + throws IOException + { + int numSaved = 0; + // Set up strings for start and end of track segment + String trackStart = "\t\n\t\ttrack\n\t\t\n\t\t\n"; + if (inAbsoluteAltitudes) { + trackStart += "\t\t\t1\n\t\t\tabsolute\n"; + } + else { + trackStart += "\t\t\tclampToGround\n"; + } + String trackEnd = "\n\t\t\n\t\n"; + + boolean justSelection = _pointTypeSelector.getJustSelection(); + + // Start segment + inWriter.write(trackStart); + StringBuilder whenList = new StringBuilder(); + StringBuilder coordList = new StringBuilder(); + + // Loop over track points + boolean firstTrackpoint = true; + final int numPoints = _track.getNumPoints(); + for (int i=0; i=inSelStart && i<=inSelEnd); + if (!point.isWaypoint() && writeCurrentPoint) + { + // start new track segment if necessary + if (point.getSegmentStart() && !firstTrackpoint) + { + inWriter.write(whenList.toString()); + inWriter.write('\n'); + inWriter.write(coordList.toString()); + inWriter.write('\n'); + inWriter.write(trackEnd); + whenList.setLength(0); coordList.setLength(0); + inWriter.write(trackStart); + } + if (point.getPhoto() == null) + { + // Add timestamp (if any) to the list + whenList.append(""); + if (point.hasTimestamp()) { + whenList.append(point.getTimestamp().getText(Timestamp.FORMAT_ISO_8601)); } - if (point.getPhoto() == null) - { - exportTrackpoint(point, inWriter); - numSaved++; - firstTrackpoint = false; + whenList.append("\n"); + // Add coordinates to the list + coordList.append(""); + coordList.append(point.getLongitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT)).append(' '); + coordList.append(point.getLatitude().output(Coordinate.FORMAT_DECIMAL_FORCE_POINT)).append(' '); + if (point.hasAltitude()) { + coordList.append("" + point.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES)); } + else { + coordList.append('0'); + } + coordList.append("\n"); + numSaved++; + firstTrackpoint = false; } } - // end segment - inWriter.write(trackEnd); } - inWriter.write("\n"); + // end segment + inWriter.write(whenList.toString()); + inWriter.write('\n'); + inWriter.write(coordList.toString()); + inWriter.write('\n'); + inWriter.write(trackEnd); return numSaved; } + /** * Reverse the hex code for the colours for KML's stupid backwards format * @param inCode colour code rrggbb @@ -652,7 +828,7 @@ public class KmlExporter extends GenericFunction implements Runnable inWriter.write(','); // Altitude if point has one if (inPoint.hasAltitude()) { - inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES)); + inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES)); } else { inWriter.write('0'); @@ -674,7 +850,7 @@ public class KmlExporter extends GenericFunction implements Runnable // Altitude if point has one inWriter.write(','); if (inPoint.hasAltitude()) { - inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES)); + inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES)); } else { inWriter.write('0'); @@ -686,10 +862,9 @@ public class KmlExporter extends GenericFunction implements Runnable /** * Loop through the photos and create thumbnails * @param inZipStream zip stream to save image files to - * @param inThumbWidth thumbnail width - * @param inThumbHeight thumbnail height + * @param inThumbSize thumbnail size */ - private void exportThumbnails(ZipOutputStream inZipStream, int inThumbWidth, int inThumbHeight) + private void exportThumbnails(ZipOutputStream inZipStream, int inThumbSize) throws IOException { // set up image writer @@ -724,9 +899,9 @@ public class KmlExporter extends GenericFunction implements Runnable // Load image and write to outstream ImageIcon icon = point.getPhoto().createImageIcon(); - // Scale image to required size TODO: should it also be smoothed, or only if it's smaller than a certain size? + // Scale image to required size (not smoothed) BufferedImage bufferedImage = ImageUtils.rotateImage(icon.getImage(), - inThumbWidth, inThumbHeight, point.getPhoto().getRotationDegrees()); + inThumbSize, inThumbSize, point.getPhoto().getRotationDegrees()); // Store image dimensions so that it doesn't have to be calculated again for the points _imageDimensions[i] = new Dimension(bufferedImage.getWidth(), bufferedImage.getHeight()); diff --git a/tim/prune/save/MapGrouter.java b/tim/prune/save/MapGrouter.java new file mode 100644 index 0000000..65e23c0 --- /dev/null +++ b/tim/prune/save/MapGrouter.java @@ -0,0 +1,139 @@ +package tim.prune.save; + +import tim.prune.config.Config; +import tim.prune.data.DoubleRange; +import tim.prune.data.Track; +import tim.prune.data.TrackExtents; +import tim.prune.gui.map.DiskTileCacher; +import tim.prune.gui.map.MapSource; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.image.BufferedImage; + + +/** + * Class to handle the sticking together (grouting) of map tiles + * to create a single map image for the current track + */ +public abstract class MapGrouter +{ + /** The most recently produced image */ + private static GroutedImage _lastGroutedImage = null; + + /** + * Clear the last image, it's not needed any more + */ + public static void clearMapImage() { + _lastGroutedImage = null; + } + + /** + * Grout the required map tiles together according to the track's extent + * @param inTrack track object + * @param inMapSource map source to use (may have one or two layers) + * @param inZoom selected zoom level + * @return grouted image, or null if no image could be created + */ + public static GroutedImage createMapImage(Track inTrack, MapSource inMapSource, int inZoom) + { + // Get the extents of the track including a standard (10%) border around the data + TrackExtents extents = new TrackExtents(inTrack); + extents.applySquareBorder(); + DoubleRange xRange = extents.getXRange(); + DoubleRange yRange = extents.getYRange(); + + // Get path to disk cache + final String cachePath = Config.getConfigString(Config.KEY_DISK_CACHE); + // Work out which tiles are required + final int zoomFactor = 1 << inZoom; + final int minTileX = (int) (xRange.getMinimum() * zoomFactor); + final int maxTileX = (int) (xRange.getMaximum() * zoomFactor); + final int minTileY = (int) (yRange.getMinimum() * zoomFactor); + final int maxTileY = (int) (yRange.getMaximum() * zoomFactor); + + // Work out how big the final image will be, create a BufferedImage + final int pixCount = (int) (extents.getXRange().getRange() * zoomFactor * 256); + if (pixCount < 2) {return null;} + BufferedImage resultImage = new BufferedImage(pixCount, pixCount, BufferedImage.TYPE_INT_RGB); + Graphics g = resultImage.getGraphics(); + g.setColor(Color.WHITE); + g.fillRect(0, 0, pixCount, pixCount); + // Work out where to start drawing the tiles on the image + int xOffset = (int) ((minTileX - xRange.getMinimum() * zoomFactor) * 256); + + int numTilesUsed = 0; + int numTilesMissing = 0; + // Loop over the tiles + for (int x = minTileX; x <= maxTileX; x++) + { + int yOffset = (int) ((minTileY - yRange.getMinimum() * zoomFactor) * 256); + for (int y = minTileY; y <= maxTileY; y++) + { + for (int layer=0; layer < inMapSource.getNumLayers(); layer++) + { + Image tile = DiskTileCacher.getTile(cachePath, inMapSource.makeFilePath(layer, inZoom, x, y), false); + if (tile != null) + { + // Wait until tile is available (loaded asynchronously) + while (tile.getWidth(null) < 0) { + try { + Thread.sleep(100); + } + catch (InterruptedException ie) {} + } + // work out where to copy it to, paint it + // System.out.println("Painting tile " + x + "," + y + " at " + xOffset + "," + yOffset); + numTilesUsed++; + g.drawImage(tile, xOffset, yOffset, null); + } + else numTilesMissing++; + } + yOffset += 256; + } + xOffset += 256; + } + // Get rid of the image if it's empty + if (numTilesUsed == 0) { + resultImage = null; + } + // Store the xy limits in the GroutedImage to make it easier to draw on top + GroutedImage result = new GroutedImage(resultImage, numTilesUsed, numTilesMissing); + result.setXRange(xRange); + result.setYRange(yRange); + return result; + } + + /** + * Get the grouted map image, using the previously-created one if available + * @param inTrack track object + * @param inMapSource map source to use (may have one or two layers) + * @param inZoom selected zoom level + * @return grouted image, or null if no image could be created + */ + public static GroutedImage getMapImage(Track inTrack, MapSource inMapSource, int inZoom) + { + if (_lastGroutedImage == null) { + _lastGroutedImage = createMapImage(inTrack, inMapSource, inZoom); + } + return _lastGroutedImage; + } + + /** + * @param inTrack track object + * @param inZoom selected zoom level + * @return true if the image size is acceptable + */ + public static boolean isZoomLevelOk(Track inTrack, int inZoom) + { + // Get the extents of the track including a standard (10%) border around the data + TrackExtents extents = new TrackExtents(inTrack); + extents.applySquareBorder(); + + // Work out how big the final image will be + final int zoomFactor = 1 << inZoom; + final int pixCount = (int) (extents.getXRange().getRange() * zoomFactor * 256); + return pixCount > 2 && pixCount < 4000; + } +} diff --git a/tim/prune/save/PovExporter.java b/tim/prune/save/PovExporter.java index ee30841..ba66058 100644 --- a/tim/prune/save/PovExporter.java +++ b/tim/prune/save/PovExporter.java @@ -12,7 +12,9 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; +import javax.imageio.ImageIO; import javax.swing.BorderFactory; +import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ButtonGroup; import javax.swing.JButton; @@ -24,8 +26,10 @@ import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JTextField; import javax.swing.SwingConstants; +import javax.swing.border.EtchedBorder; import tim.prune.App; +import tim.prune.DataSubscriber; import tim.prune.I18nManager; import tim.prune.UpdateMessageBroker; import tim.prune.config.Config; @@ -33,14 +37,16 @@ import tim.prune.data.NumberUtils; import tim.prune.data.Track; import tim.prune.function.Export3dFunction; import tim.prune.gui.DialogCloser; +import tim.prune.gui.map.MapSource; +import tim.prune.gui.map.MapSourceLibrary; import tim.prune.load.GenericFileFilter; -import tim.prune.threedee.LineDialog; import tim.prune.threedee.ThreeDModel; /** * Class to export a 3d scene of the track to a specified Pov file + * Note: Subscriber interface only used for callback from image config dialog */ -public class PovExporter extends Export3dFunction +public class PovExporter extends Export3dFunction implements DataSubscriber { private Track _track = null; private JDialog _dialog = null; @@ -49,9 +55,13 @@ public class PovExporter extends Export3dFunction private JTextField _cameraXField = null, _cameraYField = null, _cameraZField = null; private JTextField _fontName = null, _altitudeFactorField = null; private JRadioButton _ballsAndSticksButton = null; + private JLabel _baseImageLabel = null; + private JButton _baseImageButton = null; + private BaseImageConfigDialog _baseImageConfig = null; // defaults private static final double DEFAULT_CAMERA_DISTANCE = 30.0; + private static final double MODEL_SCALE_FACTOR = 20.0; private static final String DEFAULT_FONT_FILE = "crystal.ttf"; @@ -84,10 +94,10 @@ public class PovExporter extends Export3dFunction double cameraDist = Math.sqrt(inX*inX + inY*inY + inZ*inZ); if (cameraDist > 0.0) { - _cameraX = NumberUtils.formatNumber(inX / cameraDist * DEFAULT_CAMERA_DISTANCE, 5); - _cameraY = NumberUtils.formatNumber(inY / cameraDist * DEFAULT_CAMERA_DISTANCE, 5); + _cameraX = NumberUtils.formatNumberUk(inX / cameraDist * DEFAULT_CAMERA_DISTANCE, 5); + _cameraY = NumberUtils.formatNumberUk(inY / cameraDist * DEFAULT_CAMERA_DISTANCE, 5); // Careful! Need to convert from java3d (right-handed) to povray (left-handed) coordinate system! - _cameraZ = NumberUtils.formatNumber(-inZ / cameraDist * DEFAULT_CAMERA_DISTANCE, 5); + _cameraZ = NumberUtils.formatNumberUk(-inZ / cameraDist * DEFAULT_CAMERA_DISTANCE, 5); } } @@ -104,12 +114,18 @@ public class PovExporter extends Export3dFunction _dialog.setLocationRelativeTo(_parentFrame); _dialog.getContentPane().add(makeDialogComponents()); } + // Make base image dialog + if (_baseImageConfig == null) + { + _baseImageConfig = new BaseImageConfigDialog(this, _dialog, _track); + } // Set angles _cameraXField.setText(_cameraX); _cameraYField.setText(_cameraY); _cameraZField.setText(_cameraZ); _altitudeFactorField.setText("" + _altFactor); + updateBaseImageDetails(); // Show dialog _dialog.pack(); _dialog.setVisible(true); @@ -123,7 +139,7 @@ public class PovExporter extends Export3dFunction private Component makeDialogComponents() { JPanel panel = new JPanel(); - panel.setLayout(new BorderLayout()); + panel.setLayout(new BorderLayout(4, 4)); JLabel introLabel = new JLabel(I18nManager.getText("dialog.exportpov.text")); introLabel.setBorder(BorderFactory.createEmptyBorder(4, 4, 6, 4)); panel.add(introLabel, BorderLayout.NORTH); @@ -135,6 +151,7 @@ public class PovExporter extends Export3dFunction public void actionPerformed(ActionEvent e) { doExport(); + MapGrouter.clearMapImage(); _dialog.dispose(); } }); @@ -143,6 +160,7 @@ public class PovExporter extends Export3dFunction cancelButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { + MapGrouter.clearMapImage(); _dialog.dispose(); } }); @@ -205,37 +223,78 @@ public class PovExporter extends Export3dFunction group.add(_ballsAndSticksButton); group.add(tubesButton); stylePanel.add(radioPanel); - // add this grid to the holder panel + // Panel for the base image + JPanel imagePanel = new JPanel(); + imagePanel.setLayout(new BorderLayout(10, 4)); + imagePanel.add(new JLabel(I18nManager.getText("dialog.exportpov.baseimage") + ": "), BorderLayout.WEST); + _baseImageLabel = new JLabel("Typical sourcename"); + imagePanel.add(_baseImageLabel, BorderLayout.CENTER); + _baseImageButton = new JButton(I18nManager.getText("button.edit")); + _baseImageButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent event) { + changeBaseImage(); + } + }); + imagePanel.add(_baseImageButton, BorderLayout.EAST); + // Put these image controls inside a holder panel with an outline + JPanel imageHolderPanel = new JPanel(); + imageHolderPanel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(4, 4, 4, 4)) + ); + imageHolderPanel.setLayout(new BorderLayout()); + imageHolderPanel.add(imagePanel, BorderLayout.NORTH); + + // add these panels to the holder panel JPanel holderPanel = new JPanel(); holderPanel.setLayout(new BorderLayout(5, 5)); JPanel boxPanel = new JPanel(); boxPanel.setLayout(new BoxLayout(boxPanel, BoxLayout.Y_AXIS)); boxPanel.add(centralPanel); + boxPanel.add(Box.createVerticalStrut(4)); boxPanel.add(stylePanel); + boxPanel.add(Box.createVerticalStrut(4)); + boxPanel.add(imageHolderPanel); holderPanel.add(boxPanel, BorderLayout.CENTER); - // 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(); - } - }); - JPanel flowPanel = new JPanel(); - flowPanel.setLayout(new FlowLayout()); - flowPanel.add(showLinesButton); - holderPanel.add(flowPanel, BorderLayout.EAST); panel.add(holderPanel, BorderLayout.CENTER); return panel; } + /** + * Change the base image by calling the BaseImageConfigDialog + */ + private void changeBaseImage() + { + // Check if there is a cache to use + if (BaseImageConfigDialog.isImagePossible()) + { + // Show new dialog to choose image details + _baseImageConfig.begin(); + } + else { + _app.showErrorMessage(getNameKey(), "dialog.exportimage.noimagepossible"); + } + } + + /** + * Update the description label according to the selected base image details + */ + private void updateBaseImageDetails() + { + String desc = null; + if (_baseImageConfig.useImage()) + { + MapSource source = MapSourceLibrary.getSource(_baseImageConfig.getSourceIndex()); + if (source != null) { + desc = source.getName() + " (" + + _baseImageConfig.getZoomLevel() + ")"; + } + } + if (desc == null) { + desc = I18nManager.getText("dialog.about.no"); + } + _baseImageLabel.setText(desc); + } /** * Select the file and export data to it @@ -267,25 +326,27 @@ public class PovExporter extends Export3dFunction if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION) { // OK pressed and file chosen - File file = _fileChooser.getSelectedFile(); - if (!file.getName().toLowerCase().endsWith(".pov")) + File povFile = _fileChooser.getSelectedFile(); + if (!povFile.getName().toLowerCase().endsWith(".pov")) { - file = new File(file.getAbsolutePath() + ".pov"); + povFile = new File(povFile.getAbsolutePath() + ".pov"); } - // Check if file exists and if necessary prompt for overwrite + final int nameLen = povFile.getName().length() - 4; + final File imageFile = new File(povFile.getParentFile(), povFile.getName().substring(0, nameLen) + "_base.png"); + final boolean imageExists = _baseImageConfig.useImage() && imageFile.exists(); + // Check if files exist and if necessary prompt for overwrite Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")}; - if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame, + if ((!povFile.exists() && !imageExists) || JOptionPane.showOptionDialog(_parentFrame, I18nManager.getText("dialog.save.overwrite.text"), I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1]) == JOptionPane.YES_OPTION) { // Export the file - if (exportFile(file)) + if (exportFile(povFile, imageFile)) { - // file saved - // Store directory in config for later - Config.setConfigString(Config.KEY_TRACK_DIR, file.getParentFile().getAbsolutePath()); + // file saved - store directory in config for later + Config.setConfigString(Config.KEY_TRACK_DIR, povFile.getParentFile().getAbsolutePath()); } else { @@ -305,10 +366,11 @@ public class PovExporter extends Export3dFunction /** * Export the track data to the specified file - * @param inFile File object to save to + * @param inPovFile File object to save pov file to + * @param inImageFile file object to save image to * @return true if successful */ - private boolean exportFile(File inFile) + private boolean exportFile(File inPovFile, File inImageFile) { FileWriter writer = null; // find out the line separator for this system @@ -317,6 +379,7 @@ public class PovExporter extends Export3dFunction { // create and scale model ThreeDModel model = new ThreeDModel(_track); + model.setModelSize(MODEL_SCALE_FACTOR); try { // try to use given altitude cap @@ -328,12 +391,28 @@ public class PovExporter extends Export3dFunction } model.scale(); - // Create file and write basics - writer = new FileWriter(inFile); - writeStartOfFile(writer, model.getModelSize(), lineSeparator); + boolean useImage = _baseImageConfig.useImage(); + if (useImage) + { + // Get base image from grouter + MapSource mapSource = MapSourceLibrary.getSource(_baseImageConfig.getSourceIndex()); + GroutedImage baseImage = MapGrouter.getMapImage(_track, mapSource, _baseImageConfig.getZoomLevel()); + try + { + useImage = ImageIO.write(baseImage.getImage(), "png", inImageFile); + } + catch (IOException ioe) { + System.err.println("Can't write image: " + ioe.getClass().getName()); + useImage = false; + } + if (!useImage) { + _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage"); + } + } - // write out lat/long lines using model - writeLatLongLines(writer, model, lineSeparator); + // Create file and write basics + writer = new FileWriter(inPovFile); + writeStartOfFile(writer, lineSeparator, useImage ? inImageFile : null); // write out points if (_ballsAndSticksButton.isSelected()) { @@ -346,7 +425,7 @@ public class PovExporter extends Export3dFunction // everything worked UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1") + " " + _track.getNumPoints() + " " + I18nManager.getText("confirm.save.ok2") - + " " + inFile.getAbsolutePath()); + + " " + inPovFile.getAbsolutePath()); return true; } catch (IOException ioe) @@ -370,11 +449,11 @@ public class PovExporter extends Export3dFunction /** * Write the start of the Pov file, including base plane and lights * @param inWriter Writer to use for writing file - * @param inModelSize model size * @param inLineSeparator line separator to use + * @param inImageFile image file to reference (or null if none) * @throws IOException on file writing error */ - private void writeStartOfFile(FileWriter inWriter, double inModelSize, String inLineSeparator) + private void writeStartOfFile(FileWriter inWriter, String inLineSeparator, File inImageFile) throws IOException { inWriter.write("// Pov file produced by GpsPrune - see http://activityworkshop.net/"); @@ -389,6 +468,20 @@ public class PovExporter extends Export3dFunction else { Config.setConfigString(Config.KEY_POVRAY_FONT, fontPath); } + + // Make the definition of the base plane depending on whether there's an image or not + final boolean useImage = (inImageFile != null); + final String boxDefinition = (inImageFile == null ? + " <-10.0, -0.15, -10.0>," + inLineSeparator + + " <10.0, 0.15, 10.0>" + inLineSeparator + + " pigment { color rgb <0.5 0.75 0.8> }" + : + " <0, 0, 0>, <1, 1, 0.001>" + inLineSeparator + + " pigment {image_map { png \"" + inImageFile.getName() + "\" map_type 0 interpolate 2 once } }" + inLineSeparator + + " scale 20.0 rotate <90, 0, 0>" + inLineSeparator + + " translate <-10.0, 0, -10.0>"); + // TODO: Maybe could use the same geometry for the imageless case, would simplify code a bit + // Set up output String[] outputLines = { "global_settings { ambient_light rgb <4, 4, 4> }", "", @@ -401,27 +494,15 @@ public class PovExporter extends Export3dFunction "}", "", // global declares "// Global declares", - "#declare lat_line =", - " cylinder {", - " <-" + inModelSize + ", 0.1, 0>,", - " <" + inModelSize + ", 0.1, 0>,", - " 0.1 // Radius", - " pigment { color rgb <0.5 0.5 0.5> }", - " }", - "#declare lon_line =", - " cylinder {", - " <0, 0.1, -" + inModelSize + ">,", - " <0, 0.1, " + inModelSize + ">,", - " 0.1 // Radius", - " pigment { color rgb <0.5 0.5 0.5> }", - " }", "#declare point_rod =", " cylinder {", " <0, 0, 0>,", " <0, 1, 0>,", " 0.15", " open", - " pigment { color rgb <0.5 0.5 0.5> }", + " texture {", + " pigment { color rgb <0.5 0.5 0.5> }", + useImage ? " } no_shadow" : " }", " }", "", // MAYBE: Export rods to POV? How to store in data? "#declare waypoint_sphere =", @@ -430,7 +511,7 @@ public class PovExporter extends Export3dFunction " texture {", " pigment {color rgb <0.1 0.1 1.0>}", " finish { phong 1 }", - " }", + useImage ? " } no_shadow" : " }", " }", "#declare track_sphere0 =", " sphere {", @@ -491,31 +572,29 @@ public class PovExporter extends Export3dFunction "#declare wall_colour = rgbt <0.5, 0.5, 0.5, 0.3>;", "", "// Base plane", "box {", - " <-" + inModelSize + ", -0.15, -" + inModelSize + ">, // Near lower left corner", - " <" + inModelSize + ", 0.15, " + inModelSize + "> // Far upper right corner", - " pigment { color rgb <0.5 0.75 0.8> }", + boxDefinition, "}", "", // write cardinals "// Cardinal letters N,S,E,W", "text {", " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.n") + "\" 0.3, 0", " pigment { color rgb <1 1 1> }", - " translate <0, 0.2, " + inModelSize + ">", + " translate <0, 0.2, 10.0>", "}", "text {", " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.s") + "\" 0.3, 0", " pigment { color rgb <1 1 1> }", - " translate <0, 0.2, -" + inModelSize + ">", + " translate <0, 0.2, -10.0>", "}", "text {", " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.e") + "\" 0.3, 0", " pigment { color rgb <1 1 1> }", - " translate <" + (inModelSize * 0.97) + ", 0.2, 0>", + " translate <9.7, 0.2, 0>", "}", "text {", " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.w") + "\" 0.3, 0", " pigment { color rgb <1 1 1> }", - " translate <-" + (inModelSize * 1.03) + ", 0.2, 0>", + " translate <-10.3, 0.2, 0>", "}", "", // MAYBE: Light positions should relate to model size "// lights", @@ -534,36 +613,6 @@ public class PovExporter extends Export3dFunction } - /** - * Write out all the lat and long lines to the file - * @param inWriter Writer to use for writing file - * @param inModel model object for getting lat/long lines - * @param inLineSeparator line separator to use - * @throws IOException on file writing error - */ - private static void writeLatLongLines(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator) - throws IOException - { - inWriter.write("// Latitude and longitude lines:"); - inWriter.write(inLineSeparator); - int numlines = inModel.getLatitudeLines().length; - for (int i=0; i }"); - inWriter.write(inLineSeparator); - } - numlines = inModel.getLongitudeLines().length; - for (int i=0; i }"); - inWriter.write(inLineSeparator); - } - inWriter.write(inLineSeparator); - } - - /** * Write out all the data points to the file in the balls-and-sticks style * @param inWriter Writer to use for writing file @@ -829,4 +878,16 @@ public class PovExporter extends Export3dFunction } return segmentList; } + + /** + * Callback from base image config dialog + */ + public void dataUpdated(byte inUpdateType) + { + updateBaseImageDetails(); + } + + /** Not required */ + public void actionCompleted(String inMessage) { + } } diff --git a/tim/prune/save/SvgExporter.java b/tim/prune/save/SvgExporter.java index 4a86c5b..33f42f9 100644 --- a/tim/prune/save/SvgExporter.java +++ b/tim/prune/save/SvgExporter.java @@ -47,7 +47,7 @@ public class SvgExporter extends Export3dFunction private JTextField _phiField = null, _thetaField = null; private JTextField _altitudeFactorField = null; private JCheckBox _gradientsCheckbox = null; - private static double _scaleFactor = 1.0; + private static final double _scaleFactor = 200.0; /** @@ -265,16 +265,15 @@ public class SvgExporter extends Export3dFunction } catch (NumberFormatException nfe) {} model.scale(); - _scaleFactor = 200 / model.getModelSize(); boolean useGradients = _gradientsCheckbox.isSelected(); // Create file and write basics writer = new FileWriter(inFile); writeStartOfFile(writer, useGradients, lineSeparator); - writeBasePlane(writer, model.getModelSize(), lineSeparator); + writeBasePlane(writer, lineSeparator); // write out cardinal letters NESW - writeCardinals(writer, model.getModelSize(), lineSeparator); + writeCardinals(writer, lineSeparator); // write out points writeDataPoints(writer, model, useGradients, lineSeparator); @@ -342,18 +341,17 @@ public class SvgExporter extends Export3dFunction /** * Write the base plane * @param inWriter Writer to use for writing file - * @param inModelSize model size * @param inLineSeparator line separator to use * @throws IOException on file writing error */ - private void writeBasePlane(FileWriter inWriter, double inModelSize, String inLineSeparator) + private void writeBasePlane(FileWriter inWriter, String inLineSeparator) throws IOException { // Use model size and camera angles to draw path for base rectangle (using 3d transform) - int[] coords1 = convertCoordinates(-inModelSize, -inModelSize, 0); - int[] coords2 = convertCoordinates(inModelSize, -inModelSize, 0); - int[] coords3 = convertCoordinates(inModelSize, inModelSize, 0); - int[] coords4 = convertCoordinates(-inModelSize, inModelSize, 0); + int[] coords1 = convertCoordinates(-1.0, -1.0, 0); + int[] coords2 = convertCoordinates(1.0, -1.0, 0); + int[] coords3 = convertCoordinates(1.0, 1.0, 0); + int[] coords4 = convertCoordinates(-1.0, 1.0, 0); final String corners = "M " + coords1[0] + "," + coords1[1] + " L " + coords2[0] + "," + coords2[1] + " L " + coords3[0] + "," + coords3[1] @@ -365,21 +363,20 @@ public class SvgExporter extends Export3dFunction /** * Write the cardinal letters NESW * @param inWriter Writer to use for writing file - * @param inModelSize model size * @param inLineSeparator line separator to use * @throws IOException on file writing error */ - private void writeCardinals(FileWriter inWriter, double inModelSize, String inLineSeparator) + private void writeCardinals(FileWriter inWriter, String inLineSeparator) throws IOException { // Use model size and camera angles to calculate positions - int[] coordsN = convertCoordinates(0, inModelSize, 0); + int[] coordsN = convertCoordinates(0, 1.0, 0); writeCardinal(inWriter, coordsN[0], coordsN[1], "cardinal.n", inLineSeparator); - int[] coordsE = convertCoordinates(inModelSize, 0, 0); + int[] coordsE = convertCoordinates(1.0, 0, 0); writeCardinal(inWriter, coordsE[0], coordsE[1], "cardinal.e", inLineSeparator); - int[] coordsS = convertCoordinates(0, -inModelSize, 0); + int[] coordsS = convertCoordinates(0, -1.0, 0); writeCardinal(inWriter, coordsS[0], coordsS[1], "cardinal.s", inLineSeparator); - int[] coordsW = convertCoordinates(-inModelSize, 0, 0); + int[] coordsW = convertCoordinates(-1.0, 0, 0); writeCardinal(inWriter, coordsW[0], coordsW[1], "cardinal.w", inLineSeparator); } diff --git a/tim/prune/threedee/Java3DWindow.java b/tim/prune/threedee/Java3DWindow.java index 1671607..cfac9af 100644 --- a/tim/prune/threedee/Java3DWindow.java +++ b/tim/prune/threedee/Java3DWindow.java @@ -58,7 +58,7 @@ public class Java3DWindow implements ThreeDWindow private JFrame _frame = null; private ThreeDModel _model = null; private OrbitBehavior _orbit = null; - private double _altFactor = 50.0; + private double _altFactor = 5.0; /** only prompt about big track size once */ private static boolean TRACK_SIZE_WARNING_GIVEN = false; @@ -68,6 +68,7 @@ public class Java3DWindow implements ThreeDWindow private static final double INITIAL_X_ROTATION = 15.0; private static final String CARDINALS_FONT = "Arial"; private static final int MAX_TRACK_SIZE = 2500; // threshold for warning + private static final double MODEL_SCALE_FACTOR = 20.0; /** @@ -127,9 +128,8 @@ public class Java3DWindow implements ThreeDWindow Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")}; if (_track.getNumPoints() > MAX_TRACK_SIZE && !TRACK_SIZE_WARNING_GIVEN) { - // FIXME: Change text reference from exportpov to java3d if (JOptionPane.showOptionDialog(_parentFrame, - I18nManager.getText("dialog.exportpov.warningtracksize"), + I18nManager.getText("dialog.3d.warningtracksize"), I18nManager.getText("function.show3d"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1]) == JOptionPane.OK_OPTION) @@ -191,18 +191,7 @@ public class Java3DWindow implements ThreeDWindow } }}); panel.add(svgButton); - // Display coordinates of lat/long lines of 3d graph in separate dialog - JButton showLinesButton = new JButton(I18nManager.getText("button.showlines")); - showLinesButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) - { - double[] latLines = _model.getLatitudeLines(); - double[] lonLines = _model.getLongitudeLines(); - LineDialog dialog = new LineDialog(_frame, latLines, lonLines); - dialog.showDialog(); - } - }); - panel.add(showLinesButton); + // Close button JButton closeButton = new JButton(I18nManager.getText("button.close")); closeButton.addActionListener(new ActionListener() @@ -305,9 +294,6 @@ public class Java3DWindow implements ThreeDWindow _model.setAltitudeFactor(_altFactor); _model.scale(); - // Lat/Long lines - objTrans.addChild(createLatLongs(_model)); - // Add points to model objTrans.addChild(createDataPoints(_model)); @@ -360,70 +346,6 @@ public class Java3DWindow implements ThreeDWindow } - /** - * Create all the latitude and longitude lines on the base plane - * @param inModel model containing data - * @return Group object containing cylinders for lat and long lines - */ - private static Group createLatLongs(ThreeDModel inModel) - { - Group group = new Group(); - int numlines = inModel.getLatitudeLines().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").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").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 2c9ac97..ddf9162 100644 --- a/tim/prune/threedee/ThreeDModel.java +++ b/tim/prune/threedee/ThreeDModel.java @@ -1,6 +1,5 @@ package tim.prune.threedee; -import tim.prune.data.Altitude; import tim.prune.data.DataPoint; import tim.prune.data.PointScaler; import tim.prune.data.Track; @@ -16,12 +15,11 @@ public class ThreeDModel private PointScaler _scaler = null; private double _scaleFactor = 1.0; private double _altFactor = 1.0; + private double _externalScaleFactor = 1.0; // MAYBE: How to store rods (lifts) in data? private byte[] _pointTypes = null; private byte[] _pointHeights = null; - private static final double MODEL_SIZE = 10.0; - // Constants for point types public static final byte POINT_TYPE_WAYPOINT = 1; public static final byte POINT_TYPE_NORMAL_POINT = 2; @@ -57,6 +55,14 @@ public class ThreeDModel } } + /** + * @param inSize size of model + */ + public void setModelSize(double inSize) + { + _externalScaleFactor = inSize; + } + /** * Scale all points and calculate factors */ @@ -64,30 +70,16 @@ public class ThreeDModel { // Use PointScaler to sort out x and y values _scaler = new PointScaler(_track); - _scaler.scale(); - // Calculate scale factor to fit within box - _scaleFactor = 1.0; - if (_scaler.getMaximumHoriz() > 0.0 || _scaler.getMaximumVert() > 0.0) - { - if (_scaler.getMaximumHoriz() > _scaler.getMaximumVert()) - { - // scale limited by longitude - _scaleFactor = MODEL_SIZE / _scaler.getMaximumHoriz(); - } - else - { - // scale limited by latitude - _scaleFactor = MODEL_SIZE / _scaler.getMaximumVert(); - } - } + _scaler.scale(); // Add 10% border + // cap altitude scale factor if it's too big - double maxScaledAlt = _scaler.getMaxScaledAlt() * _altFactor; - if (maxScaledAlt > MODEL_SIZE) { + double maxAlt = _scaler.getAltitudeRange() * _altFactor; + if (maxAlt > 0.5) + { // capped - _altFactor = _altFactor * MODEL_SIZE / maxScaledAlt; + //System.out.println("Capped alt factor from " + _altFactor + " to " + (_altFactor * 0.5 / maxAlt)); + _altFactor = _altFactor * 0.5 / maxAlt; } - // calculate lat/long lines - _scaler.calculateLatLongLines(); // calculate point types and height codes calculatePointTypes(); @@ -108,7 +100,7 @@ public class ThreeDModel DataPoint point = _track.getPoint(i); _pointTypes[i] = (point.isWaypoint()?POINT_TYPE_WAYPOINT: (point.getSegmentStart()?POINT_TYPE_SEGMENT_START:POINT_TYPE_NORMAL_POINT)); - _pointHeights[i] = (byte) (point.getAltitude().getValue(Altitude.Format.METRES) / 500); + _pointHeights[i] = (byte) (point.getAltitude().getMetricValue() / 500); } } @@ -120,7 +112,7 @@ public class ThreeDModel */ public double getScaledHorizValue(int inIndex) { - return _scaler.getHorizValue(inIndex) * _scaleFactor; + return _scaler.getHorizValue(inIndex) * _scaleFactor * _externalScaleFactor; } /** @@ -130,7 +122,7 @@ public class ThreeDModel */ public double getScaledVertValue(int inIndex) { - return _scaler.getVertValue(inIndex) * _scaleFactor; + return _scaler.getVertValue(inIndex) * _scaleFactor * _externalScaleFactor; } /** @@ -144,44 +136,10 @@ public class ThreeDModel double altVal = _scaler.getAltValue(inIndex); if (altVal < 0) return 0; // scale according to exaggeration factor - return altVal * _altFactor; + return altVal * _altFactor * _externalScaleFactor; } - /** - * @return latitude lines - */ - public double[] getLatitudeLines() - { - return _scaler.getLatitudeLines(); - } - - /** - * @param inIndex index of line, starting at 0 - * @return scaled position of latitude line - */ - public double getScaledLatitudeLine(int inIndex) - { - return _scaler.getScaledLatitudeLines()[inIndex] * _scaleFactor; - } - - /** - * @return longitude lines - */ - public double[] getLongitudeLines() - { - return _scaler.getLongitudeLines(); - } - - /** - * @param inIndex index of line, starting at 0 - * @return scaled position of longitude line - */ - public double getScaledLongitudeLine(int inIndex) - { - return _scaler.getScaledLongitudeLines()[inIndex] * _scaleFactor; - } - /** * @param inIndex index of point, starting at 0 * @return point type, either POINT_TYPE_WAYPOINT or POINT_TYPE_NORMAL_POINT @@ -199,12 +157,4 @@ public class ThreeDModel { return _pointHeights[inIndex]; } - - /** - * @return the current model size - */ - public double getModelSize() - { - return MODEL_SIZE; - } } diff --git a/tim/prune/undo/UndoDeleteRange.java b/tim/prune/undo/UndoDeleteRange.java index ba2b31a..58f09e9 100644 --- a/tim/prune/undo/UndoDeleteRange.java +++ b/tim/prune/undo/UndoDeleteRange.java @@ -14,7 +14,7 @@ public class UndoDeleteRange implements UndoOperation /** * Inner class to hold a single range information set */ - class RangeInfo + static class RangeInfo { public int _startIndex = -1; public DataPoint[] _points = null; -- 2.43.0