]> gitweb.fperrin.net Git - GpsPrune.git/commitdiff
Version 15, March 2013
authoractivityworkshop <mail@activityworkshop.net>
Sun, 15 Feb 2015 10:46:36 +0000 (11:46 +0100)
committeractivityworkshop <mail@activityworkshop.net>
Sun, 15 Feb 2015 10:46:36 +0000 (11:46 +0100)
130 files changed:
tim/prune/App.java
tim/prune/FileDropHandler.java [new file with mode: 0644]
tim/prune/FunctionLibrary.java
tim/prune/GpsPrune.java
tim/prune/config/Config.java
tim/prune/correlate/MediaPreviewTableModel.java
tim/prune/data/Altitude.java
tim/prune/data/AltitudeRange.java
tim/prune/data/Checker.java
tim/prune/data/Coordinate.java
tim/prune/data/DataPoint.java
tim/prune/data/DoubleRange.java
tim/prune/data/MediaList.java
tim/prune/data/MediaObject.java
tim/prune/data/NumberUtils.java
tim/prune/data/PointCreateOptions.java [new file with mode: 0644]
tim/prune/data/PointScaler.java
tim/prune/data/RangeStats.java [new file with mode: 0644]
tim/prune/data/Selection.java
tim/prune/data/Speed.java [new file with mode: 0644]
tim/prune/data/SpeedCalculator.java
tim/prune/data/Timestamp.java
tim/prune/data/Track.java
tim/prune/data/TrackExtents.java [new file with mode: 0644]
tim/prune/data/Unit.java
tim/prune/data/UnitSet.java
tim/prune/data/UnitSetLibrary.java
tim/prune/function/AboutScreen.java
tim/prune/function/AddAltitudeOffset.java
tim/prune/function/AddMapSourceDialog.java
tim/prune/function/DownloadOsmFunction.java
tim/prune/function/Export3dFunction.java
tim/prune/function/FullRangeDetails.java
tim/prune/function/GetWikipediaFunction.java
tim/prune/function/PasteCoordinates.java
tim/prune/function/SearchWikipediaNames.java
tim/prune/function/SetKmzImageSize.java [deleted file]
tim/prune/function/SetLanguage.java
tim/prune/function/edit/EditFieldsTableModel.java
tim/prune/function/edit/PointEditor.java
tim/prune/function/edit/PointNameEditor.java
tim/prune/function/estimate/EstimateTime.java [new file with mode: 0644]
tim/prune/function/estimate/EstimationParameters.java [new file with mode: 0644]
tim/prune/function/estimate/LearnParameters.java [new file with mode: 0644]
tim/prune/function/estimate/ParametersPanel.java [new file with mode: 0644]
tim/prune/function/estimate/jama/Maths.java [new file with mode: 0644]
tim/prune/function/estimate/jama/Matrix.java [new file with mode: 0644]
tim/prune/function/estimate/jama/QRDecomposition.java [new file with mode: 0644]
tim/prune/function/gpsies/GenericDownloaderFunction.java
tim/prune/function/gpsies/TrackListModel.java
tim/prune/function/gpsies/UploadGpsiesFunction.java
tim/prune/function/srtm/LookupSrtmFunction.java
tim/prune/function/srtm/TileFinder.java
tim/prune/gui/DecimalNumberField.java [new file with mode: 0644]
tim/prune/gui/DetailsDisplay.java
tim/prune/gui/DisplayUtils.java
tim/prune/gui/GuiGridLayout.java
tim/prune/gui/IconManager.java
tim/prune/gui/MenuManager.java
tim/prune/gui/ProgressDialog.java [new file with mode: 0644]
tim/prune/gui/StatusIcon.java [new file with mode: 0644]
tim/prune/gui/WholeNumberField.java
tim/prune/gui/WizardLayout.java [new file with mode: 0644]
tim/prune/gui/images/add_photo_icon.png [changed mode: 0755->0644]
tim/prune/gui/images/add_textfile_icon.png [changed mode: 0755->0644]
tim/prune/gui/images/entry_invalid.gif [new file with mode: 0644]
tim/prune/gui/images/entry_none.gif [new file with mode: 0644]
tim/prune/gui/images/entry_valid.gif [new file with mode: 0644]
tim/prune/gui/map/MapCanvas.java
tim/prune/gui/map/MapUtils.java
tim/prune/gui/profile/AltitudeData.java
tim/prune/gui/profile/ArbitraryData.java [new file with mode: 0644]
tim/prune/gui/profile/ProfileChart.java
tim/prune/lang/prune-texts_af.properties
tim/prune/lang/prune-texts_cz.properties
tim/prune/lang/prune-texts_da.properties
tim/prune/lang/prune-texts_de.properties
tim/prune/lang/prune-texts_de_CH.properties
tim/prune/lang/prune-texts_en.properties
tim/prune/lang/prune-texts_en_US.properties
tim/prune/lang/prune-texts_es.properties
tim/prune/lang/prune-texts_fr.properties
tim/prune/lang/prune-texts_hu.properties
tim/prune/lang/prune-texts_it.properties
tim/prune/lang/prune-texts_ja.properties
tim/prune/lang/prune-texts_ko.properties
tim/prune/lang/prune-texts_nl.properties
tim/prune/lang/prune-texts_pl.properties
tim/prune/lang/prune-texts_pt.properties
tim/prune/lang/prune-texts_ru.properties
tim/prune/lang/prune-texts_tr.properties
tim/prune/lang/prune-texts_zh.properties
tim/prune/load/BabelFileFormats.java
tim/prune/load/BabelLoadFromFile.java
tim/prune/load/BabelLoadFromGps.java
tim/prune/load/BabelLoader.java
tim/prune/load/ComponentHider.java [new file with mode: 0644]
tim/prune/load/FieldGuesser.java
tim/prune/load/FileCacher.java
tim/prune/load/FileLoader.java
tim/prune/load/JpegLoader.java
tim/prune/load/MediaLoadProgressDialog.java
tim/prune/load/NmeaFileLoader.java
tim/prune/load/TextFileLoader.java
tim/prune/load/babel/AddFilterDialog.java [new file with mode: 0644]
tim/prune/load/babel/BabelFilterPanel.java [new file with mode: 0644]
tim/prune/load/babel/DiscardFilter.java [new file with mode: 0644]
tim/prune/load/babel/DistanceFilter.java [new file with mode: 0644]
tim/prune/load/babel/FilterDefinition.java [new file with mode: 0644]
tim/prune/load/babel/InterpolateFilter.java [new file with mode: 0644]
tim/prune/load/babel/SimplifyFilter.java [new file with mode: 0644]
tim/prune/load/xml/GzipFileLoader.java
tim/prune/load/xml/XmlFileLoader.java
tim/prune/load/xml/ZipFileLoader.java
tim/prune/readme.txt
tim/prune/save/BaseImageConfigDialog.java [new file with mode: 0644]
tim/prune/save/ExifSaver.java
tim/prune/save/FileSaver.java
tim/prune/save/GpxExporter.java
tim/prune/save/GroutedImage.java [new file with mode: 0644]
tim/prune/save/ImageExporter.java [new file with mode: 0644]
tim/prune/save/ImagePreviewPanel.java [new file with mode: 0644]
tim/prune/save/KmlExporter.java
tim/prune/save/MapGrouter.java [new file with mode: 0644]
tim/prune/save/PovExporter.java
tim/prune/save/SvgExporter.java
tim/prune/threedee/Java3DWindow.java
tim/prune/threedee/LineDialog.java [deleted file]
tim/prune/threedee/ThreeDModel.java
tim/prune/undo/UndoDeleteRange.java

index 205d88ae11c74165c23bd6aa7ef1fab275af4485..1f223265b0aa929e9099a6d7aba71ea5b0a49a0d 100644 (file)
@@ -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 (file)
index 0000000..2c76385
--- /dev/null
@@ -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<File> dataFiles = new ArrayList<File>();
+
+               // Try a java file list flavour first
+               try
+               {
+                       @SuppressWarnings("unchecked")
+                       java.util.List<File> fileList = (java.util.List<File>)
+                       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<File> 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);
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+}
index f06dbe457983ad723c8f00ee9f3c8b797517b656..53d8563cc33587d4303d945ff2d51fd44cca57fa 100644 (file)
@@ -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);
index 70e1c8e7858669816843165d5dfd386b006b62ed..3214baca31fc3bfd7cdfd6a17a40375c4ac1c4b2 100644 (file)
@@ -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=<file> used to specify a configuration file"
                                + "\n   --lang=<code>       used to specify language code such as DE"
                                + "\n   --langfile=<file>   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);
index 5a95bebdebc62d52c6da4f52ec7391026781c206..385a5d9f12dc64d387711cf54abc4fbbb5644633 100644 (file)
@@ -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;
index 3d6efa8196c1ca57cf47aced06d83b211577c5f8..e562ff9f4ff32fc764f70bbec9f6ea09207caa32 100644 (file)
@@ -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<MediaPreviewTableRow> _list = new ArrayList<MediaPreviewTableRow>();
        /** 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 "";
                }
index ac71d759cb4a45a8a7af971b8d6adc830010bf83..7d9c21cb6c414a5f4d16503e1f6bce0dbf215a73 100644 (file)
@@ -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);
        }
 }
index b7ec647ab6456f4ad9bdef0f625d513fa28360d4..d53223e9ad09589e5ace7bb8726c8a68711ac6a0 100644 (file)
@@ -118,4 +118,12 @@ public class AltitudeRange
        {
                return (int) (_descent * inUnit.getMultFactorFromStd());
        }
+
+       /**
+        * @return overall height gain in metres
+        */
+       public double getMetricHeightDiff()
+       {
+               return _climb - _descent;
+       }
 }
index 274c3e70f27977821ce1a3a559ffa1f83033e230..085f50f88dcff01f33bff213f453953cef30f4a7 100644 (file)
@@ -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<halfNum; i++)
index a230e775a21424a9d1fcc06aa004d0b35e92d015..085c502b363a5c81dfe18d45ebd583a92a3360c2 100644 (file)
@@ -84,7 +84,7 @@ public abstract class Coordinate
 
                        // count numeric fields - 1=d, 2=dm, 3=dm.m/dms, 4=dms.s
                        int numFields = 0;
-                       boolean inNumeric = false;
+                       boolean isNumeric = false;
                        char currChar;
                        long[] fields = new long[4]; // needs to be long for lengthy decimals
                        long[] denoms = new long[4];
@@ -97,9 +97,9 @@ public abstract class Coordinate
                                        currChar = inString.charAt(i);
                                        if (currChar >= '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;}
                                        }
index 34d7bd72055275f81339e1fe23c434d2008603c6..4de97ef219bb60f05153dc4f3a4cbbdf72a2e672 100644 (file)
@@ -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;
        }
 
index b6b8a8fb9cb9c7f1b647c9ef20d1d5779bbad16c..4e361224f420ed32b72473edc1e5adf87d37a3f0 100644 (file)
@@ -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);
+       }
 }
index c0f4408f3637a23e932679bd441449928ea5777e..cc30325f8cc12751e2943dae6da6540bba4e953f 100644 (file)
@@ -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
         */
index 478618e4a5ac330795df9684600c4a54f3452e9c..8c53cafac220580338375300a6edbc583cacbd63 100644 (file)
@@ -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)
         */
index 0c76981a12eab7b41da418514eace1e5e824c0e3..652e8243af2fc85d01dfb2940bc007bc0c755927 100644 (file)
@@ -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 (file)
index 0000000..2f61336
--- /dev/null
@@ -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)");
+       }
+}
index 180f3a5aef74e131d0c9bab9649310b4e840ef5e..a1338857e0c509acc6a3d6227f4044080e3f2914 100644 (file)
@@ -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<numPoints; p++)
-                       {
-                               point = _track.getPoint(p);
-                               if (point != null)
-                               {
-                                       latRange.addValue(point.getLatitude().getDouble());
-                                       lonRange.addValue(point.getLongitude().getDouble());
-                                       altRange.addValue(point.getAltitude().getValue(Altitude.Format.METRES));
-                               }
-                       }
+                       _xValues = new double[numPoints];
+                       _yValues = new double[numPoints];
+                       _altValues = new double[numPoints];
+               }
 
-                       // Find median latitude and calculate factor
-                       _latMedian = (latRange.getMinimum() + latRange.getMaximum()) / 2;
-                       _lonMedian = (lonRange.getMinimum() + lonRange.getMaximum()) / 2;
-                       _minAltitude = (int) altRange.getMinimum();
-                       _longFactor = Math.cos(_latMedian / 180.0 * Math.PI); // quite rough
-                       // Find altitude scale factor using distance
-                       DataPoint p1 = new DataPoint(new Latitude(latRange.getMinimum(), Coordinate.FORMAT_DEG),
-                               new Longitude(_lonMedian, Coordinate.FORMAT_DEG), null);
-                       DataPoint p2 = new DataPoint(new Latitude(latRange.getMaximum(), Coordinate.FORMAT_DEG),
-                               new Longitude(_lonMedian, Coordinate.FORMAT_DEG), null);
-                       double horizDist = Distance.convertRadiansToDistance(
-                               DataPoint.calculateRadiansBetween(p1, p2), UnitSetLibrary.UNITS_METRES); // both in m
-                       _altFactor = 1.0 / horizDist;
+               final double midXvalue = extents.getXRange().getMidValue();
+               final double midYvalue = extents.getYRange().getMidValue();
+               final double xyRange   = extents.getXRange().getRange();
 
-                       // create new arrays for scaled values
-                       if (_xValues == null || _xValues.length != numPoints)
-                       {
-                               _xValues = new double[numPoints];
-                               _yValues = new double[numPoints];
-                               _altValues = new double[numPoints];
-                       }
-                       // Calculate scaled values
-                       for (p=0; p<numPoints; p++)
+               // Calculate scaled values
+               for (int p=0; p<numPoints; p++)
+               {
+                       DataPoint point = _track.getPoint(p);
+                       if (point != null)
                        {
-                               point = _track.getPoint(p);
-                               if (point != null)
-                               {
-                                       _xValues[p] = getScaledLongitude(point.getLongitude().getDouble());
-                                       _yValues[p] = getScaledLatitude(point.getLatitude().getDouble());
-                                       _altValues[p] = getScaledAltitude(point.getAltitude());
-                                       if (_altValues[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<numLatLines; i++) _latLinesScaled[i] = getScaledLatitude(_latLinesDegs[i]);
-               _lonLinesScaled = new double[numLonLines];
-               for (int i=0; i<numLonLines; i++) _lonLinesScaled[i] = getScaledLongitude(_lonLinesDegs[i]);
-       }
-
-
-       /**
-        * Calculate the number of lines in the given range using the specified separation
-        * @param inMin minimum value
-        * @param inMax maximum value
-        * @param inSeparation line separation
-        * @return number of lines
-        */
-       private static int getNumLinesBetween(double inMin, double inMax, double inSeparation)
-       {
-               // 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) 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 (file)
index 0000000..0ed8fc7
--- /dev/null
@@ -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;
+       }
+}
index f5c41ffd91f6015d8d4762e546fe8385c5dadc8e..6125eacad76e7b0390daef85fa11e14bd65573fb 100644 (file)
@@ -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 (file)
index 0000000..80cb7df
--- /dev/null
@@ -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;
+       }
+}
index d83a7bd306ff9f72aa480f2ce0619ce0f5239dbb..f6f429c7d477bc13cb29a5a9bb3212e467e99fcf 100644 (file)
@@ -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
index 477a7227125a0a82a7a4fcd4a7c899d2352bc03a..623d80497eb995e36073e31e6067926192e9c626 100644 (file)
@@ -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;
        }
index b28fd3642091cdc485e1a9130a72c3f2ecd14dac..8623c587ba5c9131d9051ad083cdcafffd24e54c 100644 (file)
@@ -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 (file)
index 0000000..0bf3081
--- /dev/null
@@ -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<numPoints; i++)
+               {
+                       DataPoint p = _track.getPoint(i);
+                       if (p != null && p.hasAltitude()) {
+                               altRange.addValue(p.getAltitude().getMetricValue());
+                       }
+               }
+               return altRange;
+       }
+
+       /**
+        * @return the greater of the N/S and E/W extent of the track, in metres (including border)
+        */
+       public double getHorizontalDistanceMetres()
+       {
+               DoubleRange lonRange = _track.getLonRange();
+               DoubleRange latRange = _track.getLatRange();
+
+               // Find horizontal and vertical extents of enclosing rectangle
+               DataPoint southPoint = new DataPoint(new Latitude(latRange.getMinimum(), Coordinate.FORMAT_DEG),
+                       new Longitude(lonRange.getMidValue(), Coordinate.FORMAT_DEG), null);
+               DataPoint northPoint = new DataPoint(new Latitude(latRange.getMaximum(), Coordinate.FORMAT_DEG),
+                       new Longitude(lonRange.getMidValue(), Coordinate.FORMAT_DEG), null);
+               double nsDist = Distance.convertRadiansToDistance(
+                       DataPoint.calculateRadiansBetween(northPoint, southPoint), UnitSetLibrary.UNITS_METRES); // both in m
+               // Same again for bottom and top, take maximum
+               DataPoint westPoint = new DataPoint(new Latitude(latRange.getMidValue(), Coordinate.FORMAT_DEG),
+                       new Longitude(lonRange.getMinimum(), Coordinate.FORMAT_DEG), null);
+               DataPoint eastPoint = new DataPoint(new Latitude(latRange.getMidValue(), Coordinate.FORMAT_DEG),
+                       new Longitude(lonRange.getMinimum(), Coordinate.FORMAT_DEG), null);
+               double ewDist = Distance.convertRadiansToDistance(
+                       DataPoint.calculateRadiansBetween(westPoint, eastPoint), UnitSetLibrary.UNITS_METRES); // both in m
+               final double horizDistance = Math.max(nsDist, ewDist) * BORDER_MULTIPLIER;
+
+               return horizDistance;
+       }
+}
index b4e82e34cfa8c8e1e48b2e92912f3f1f41abdbc2..a073297a09b520245c53c4cabf6f526745124742 100644 (file)
@@ -39,9 +39,20 @@ public class Unit
         * @param inSuffix suffix to name key
         */
        public Unit(Unit inParent, String inSuffix)
+       {
+               this(inParent, inSuffix, 1.0);
+       }
+
+       /**
+        * Unit constructor
+        * @param inParent parent unit
+        * @param inSuffix suffix to name key
+        * @param inFactor additional time factor to apply
+        */
+       public Unit(Unit inParent, String inSuffix, double inFactor)
        {
                _nameKey = inParent._nameKey + inSuffix;
-               _multFactorFromStd = inParent._multFactorFromStd;
+               _multFactorFromStd = inParent._multFactorFromStd * inFactor;
                _isStandard = inParent._isStandard;
        }
 
index bd53e1b98b6da732945e977981b874281ff12866..7239ead76177fde38ba9c1a8ad614a60f90c0d8d 100644 (file)
@@ -10,7 +10,6 @@ public class UnitSet
        private Unit _speedUnit = null;
        private Unit _altitudeUnit = null;
        private Unit _vertSpeedUnit = null;
-       private Altitude.Format _defaultAltitudeFormat = Altitude.Format.METRES;
 
        /**
         * Constructor
@@ -18,16 +17,17 @@ public class UnitSet
         * @param inDistanceUnit distance unit
         * @param inAltitudeUnit altitude unit
         * @param inAltitudeFormat default altitude format
+        * @param inSpeedUnit unit for horizontal speeds
+        * @param inVerticalSpeedUnit unit for vertical speeds
         */
        public UnitSet(String inNameKey, Unit inDistanceUnit,
-               Unit inAltitudeUnit, Altitude.Format inAltitudeFormat)
+               Unit inAltitudeUnit, Unit inSpeedUnit, Unit inVerticalSpeedUnit)
        {
                _nameKey = inNameKey;
                _distanceUnit = inDistanceUnit;
-               _speedUnit = new Unit(_distanceUnit, "perhour");
                _altitudeUnit = inAltitudeUnit;
-               _defaultAltitudeFormat = inAltitudeFormat;
-               _vertSpeedUnit = new Unit(_altitudeUnit, "persec");
+               _speedUnit = inSpeedUnit;
+               _vertSpeedUnit = inVerticalSpeedUnit;
        }
 
        /**
@@ -66,9 +66,14 @@ public class UnitSet
        }
 
        /**
-        * @return default altitude format
+        * @return default point creation options for this unit set
         */
-       public Altitude.Format getDefaultAltitudeFormat() {
-               return _defaultAltitudeFormat;
+       public PointCreateOptions getDefaultOptions()
+       {
+               PointCreateOptions options = new PointCreateOptions();
+               options.setAltitudeUnits(getAltitudeUnit());
+               options.setSpeedUnits(getSpeedUnit());
+               options.setVerticalSpeedUnits(getVerticalSpeedUnit(), true);
+               return options;
        }
 }
index b270124193a8598f3006872da4400504762a3e10..37f3cfc5f4a8f54f94eef9311ee97cf925390a4e 100644 (file)
@@ -18,11 +18,21 @@ public abstract class UnitSetLibrary
        /** Units for nautical miles */
        public static final Unit UNITS_NAUTICAL_MILES = new Unit("nauticalmiles", 1/1852.0);
 
+       // Speed units - all conversion factors from metres per second
+       public static final Unit SPEED_UNITS_METRESPERSEC = new Unit(UNITS_METRES, "persec");
+       public static final Unit SPEED_UNITS_FEETPERSEC   = new Unit(UNITS_FEET, "persec");
+       public static final Unit SPEED_UNITS_MILESPERHOUR = new Unit(UNITS_MILES, "perhour", 60.0 * 60.0);
+       public static final Unit SPEED_UNITS_KNOTS        = new Unit(UNITS_NAUTICAL_MILES, "perhour", 60.0 * 60.0);
+       public static final Unit SPEED_UNITS_KMPERHOUR    = new Unit(UNITS_KILOMETRES, "perhour", 60.0 * 60.0);
+       public static final Unit[] ALL_SPEED_UNITS = {SPEED_UNITS_METRESPERSEC, SPEED_UNITS_KMPERHOUR,
+               SPEED_UNITS_FEETPERSEC, SPEED_UNITS_MILESPERHOUR};
+
        /** Array of available unit sets */
-       private static UnitSet[] _sets = {
-               new UnitSet("unitset.kilometres", UNITS_KILOMETRES, UNITS_METRES, Altitude.Format.METRES),
-               new UnitSet("unitset.miles", UNITS_MILES, UNITS_FEET, Altitude.Format.FEET),
-               new UnitSet("unitset.nautical", UNITS_NAUTICAL_MILES, UNITS_FEET, Altitude.Format.FEET)
+       private static UnitSet[] _sets =
+       {
+               new UnitSet("unitset.kilometres", UNITS_KILOMETRES, UNITS_METRES, SPEED_UNITS_KMPERHOUR, SPEED_UNITS_METRESPERSEC),
+               new UnitSet("unitset.miles", UNITS_MILES, UNITS_FEET, SPEED_UNITS_MILESPERHOUR, SPEED_UNITS_FEETPERSEC),
+               new UnitSet("unitset.nautical", UNITS_NAUTICAL_MILES, UNITS_FEET, SPEED_UNITS_KNOTS, SPEED_UNITS_FEETPERSEC)
        };
 
        /**
@@ -53,9 +63,8 @@ public abstract class UnitSetLibrary
        public static UnitSet getUnitSet(String inKey)
        {
                // Loop over all available unit sets
-               for (int i=0; i<getNumUnitSets(); i++)
+               for (UnitSet set : _sets)
                {
-                       UnitSet set = getUnitSet(i);
                        if (set.getNameKey().equals(inKey)) {
                                return set;
                        }
index 0ec7ae6fc06ad1063531f605eff95ea2fe47054d..7a95693b734dcf0ae67bdd19c27b202ede513c6f 100644 (file)
@@ -99,7 +99,7 @@ public class AboutScreen extends GenericFunction
                descBuffer.append("<p>").append(I18nManager.getText("dialog.about.languages")).append(" : ")
                        .append("\u010de\u0161tina, deutsch, english, espa\u00F1ol, fran\u00E7ais, italiano, magyar,<br>" +
                                " nederlands, polski, portugu\u00EAs, \u0440\u0443\u0441\u0441\u043a\u0438\u0439 (russian), \u4e2d\u6587 (chinese), \u65E5\u672C\u8A9E (japanese),<br>" +
-                               " \uD55C\uAD6D\uC5B4/\uC870\uC120\uB9D0 (korean), schwiizerd\u00FC\u00FCtsch, t\u00FCrk\u00E7e, rom\u00E2n\u0103, afrikaans, bahasa indonesia</p>");
+                               " \uD55C\uAD6D\uC5B4/\uC870\uC120\uB9D0 (korean), schwiizerd\u00FC\u00FCtsch, t\u00FCrk\u00E7e, afrikaans, rom\u00E2n\u0103</p>");
                descBuffer.append("<p>").append(I18nManager.getText("dialog.about.translatedby")).append("</p>");
                JEditorPane descPane = new JEditorPane("text/html", descBuffer.toString());
                descPane.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
index 85c52dee7f5291bc8293be3aa3b5038a0068a150..c98ab3e17843a27db638d74ffac802c5400bea36 100644 (file)
@@ -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();
        }
 }
index da1f15e8a951e0d6c258041e5872d5e84c799299..52a8874cf100b355b8b0d6acac60215dcfe29202 100644 (file)
@@ -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);
index e3d362738d6814c8fc99cf6c86ec20baaaace679..76c69da7f928aa3099e383c5603a06de11997025 100644 (file)
@@ -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();
 
index 5b12125b0d20f34bae4776f3cefdce0ea41b6400..9e92e62158573d624b5f24c409c8ccdb56954544 100644 (file)
@@ -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
index a0bae6517a09419dd5af77622f49b983de0e601e..d9dbb7011253fdb4f1ac54008739259a8eb3eab5 100644 (file)
@@ -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);
-       }
 }
index 0e6d9eb5ae432454b505cc8fb8ba4a200b2bf57c..4c11bf9d13d4e0b393604939e1535d633499204f 100644 (file)
@@ -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<GpsiesTrack> 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)
         */
index 7587949b25c893e5a4c9565d69d6a723ae603ec2..0228014deb4810f025ba3d340247fe4bc920004b 100644 (file)
@@ -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:
index 0458943036ed8007ffecb0838bf66c504aff4e6d..5712983394e24882d7b1098a1eeead49d92f41e8 100644 (file)
@@ -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<GpsiesTrack> 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; i<numChars; i++)
+                       {
+                               char c = inSearchTerm.charAt(i);
+                               switch (c)
+                               {
+                                       // German umlauted vowels, add an "e"
+                                       case '\u00fc' : sb.append("ue"); break;
+                                       case '\u00e4' : sb.append("ae"); break;
+                                       case '\u00f6' : sb.append("oe"); break;
+                                       // German doppel s
+                                       case '\u00df' : sb.append("ss"); break;
+                                       // accented vowels
+                                       case '\u00e8' : case '\u00e9' :
+                                       case '\u00ea' : case '\u00eb' : sb.append('e'); break;
+                                       case '\u00e0' : case '\u00e1' :
+                                       case '\u00e2' : sb.append('a'); break;
+                                       case '\u00f2' : case '\u00f3' :
+                                       case '\u00f4' : sb.append('o'); break;
+                                       case '\u00ec' : case '\u00ed' :
+                                       case '\u00ee' : case '\u00ef' : sb.append('i'); break;
+                                       // cedillas, ny, l bar
+                                       case '\u00e7' : sb.append('c'); break;
+                                       case '\u015f' : sb.append('s'); break;
+                                       case '\u00f1' : sb.append('n'); break;
+                                       case '\u0142' : sb.append('l'); break;
+                                       // everything else
+                                       default  : sb.append(c);
+                               }
+                       }
+                       String searchTerm = inSearchTerm;
+                       try {
+                               searchTerm = URLEncoder.encode(sb.toString(), "UTF-8");
+                       } catch (UnsupportedEncodingException e1) {}
+                       // System.out.println("Converted '" + inSearchTerm + "' to '" + searchTerm + "'");
+                       return searchTerm;
                }
-               _statusLabel.setText(descMessage);
+               return "";
        }
 
-
        /**
         * Load the selected point(s)
         */
diff --git a/tim/prune/function/SetKmzImageSize.java b/tim/prune/function/SetKmzImageSize.java
deleted file mode 100644 (file)
index c63d4fe..0000000
+++ /dev/null
@@ -1,159 +0,0 @@
-package tim.prune.function;
-
-import java.awt.BorderLayout;
-import java.awt.Component;
-import java.awt.FlowLayout;
-import java.awt.GridLayout;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.KeyAdapter;
-import java.awt.event.KeyEvent;
-import java.awt.event.MouseAdapter;
-
-import javax.swing.BorderFactory;
-import javax.swing.JButton;
-import javax.swing.JDialog;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.SwingConstants;
-
-import tim.prune.App;
-import tim.prune.GenericFunction;
-import tim.prune.I18nManager;
-import tim.prune.config.Config;
-import tim.prune.gui.WholeNumberField;
-
-/**
- * Class to provide the function to set the image size for kmz output
- */
-public class SetKmzImageSize extends GenericFunction
-{
-       private JDialog _dialog = null;
-       private JButton _okButton = null;
-       private WholeNumberField _widthField = null, _heightField = null;
-
-
-       /**
-        * Constructor
-        * @param inApp application object for callback
-        */
-       public SetKmzImageSize(App inApp)
-       {
-               super(inApp);
-       }
-
-       /** Get the name key */
-       public String getNameKey() {
-               return "function.setkmzimagesize";
-       }
-
-       /**
-        * Begin the function
-        */
-       public void begin()
-       {
-               // Make dialog window
-               if (_dialog == null)
-               {
-                       _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
-                       _dialog.setLocationRelativeTo(_parentFrame);
-                       _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
-                       _dialog.getContentPane().add(makeDialogComponents());
-                       _dialog.pack();
-               }
-               // Initialise values from config
-               _widthField.setValue(Config.getConfigInt(Config.KEY_KMZ_IMAGE_WIDTH));
-               _heightField.setValue(Config.getConfigInt(Config.KEY_KMZ_IMAGE_HEIGHT));
-               _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());
-
-               // Make a central panel with the text boxes
-               JPanel mainPanel = new JPanel();
-               mainPanel.setLayout(new GridLayout(0, 2));
-               mainPanel.add(makeRightLabel("dialog.saveconfig.prune.kmzimagewidth"));
-               _widthField = new WholeNumberField(4);
-               mainPanel.add(_widthField);
-               mainPanel.add(makeRightLabel("dialog.saveconfig.prune.kmzimageheight"));
-               _heightField = new WholeNumberField(4);
-               mainPanel.add(_heightField);
-               dialogPanel.add(mainPanel, BorderLayout.CENTER);
-
-               // Listeners to enable/disable ok button
-               KeyAdapter keyListener = new KeyAdapter() {
-                       /** Key released */
-                       public void keyReleased(KeyEvent arg0) {
-                               _okButton.setEnabled(_widthField.getValue()>0 && _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();
-               }
-       }
-}
index 44078084ef4af0d7510f476d01f07086bf97f91d..3962787601f9ef789ea39d8ca108781384e41292 100644 (file)
@@ -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"
        };
 
 
index f4ee5617b22cf7eb3a77d7783b93c3901ebeab22..4fb46d7aa2d1443c3af929c1d5ffb05cd44a8ef8 100644 (file)
@@ -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
index 2321ece0bda861184eaecca8879eb134624a87ec..40cf12ba251495dfebca5a673c4f67cf6b3c816e 100644 (file)
@@ -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();
index bfa46c4bf4d54f9df5f4e9101b859903a26cd172..53e1f834b962fa8a098a9fc9345934dbdac3003f 100644 (file)
@@ -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 (file)
index 0000000..7a502e4
--- /dev/null
@@ -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 (file)
index 0000000..e78994b
--- /dev/null
@@ -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<inString.length(); i++)
+               {
+                       char c = inString.charAt(i);
+                       if (Character.isLetter(c)) {return false;} // no letters allowed
+                       currPunc = (c == '.' || c == ',');
+                       if (currPunc && prevPunc) {return false;} // no consecutive . or , allowed
+                       prevPunc = currPunc;
+                       hasDigit = hasDigit || Character.isDigit(c);
+               }
+               return hasDigit; // must have at least one digit!
+       }
+
+       /**
+        * @return true if the parameters are valid, with no parsing errors
+        */
+       public boolean isValid()
+       {
+               return !_parseFailed; // && _flatMins > 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 (file)
index 0000000..8f4ea68
--- /dev/null
@@ -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<RangeStats> statsList = new ArrayList<RangeStats>(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<RangeStats> 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<RangeStats> 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<inMatrix.getNumRows(); i++)
+               {
+                       currValue = Math.abs(inMatrix.get(i, 0));
+                       if (currValue > 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<RangeStats> 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<RangeStats> 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 (file)
index 0000000..3632831
--- /dev/null
@@ -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 (file)
index 0000000..7c3326d
--- /dev/null
@@ -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 (file)
index 0000000..face1a9
--- /dev/null
@@ -0,0 +1,259 @@
+package tim.prune.function.estimate.jama;\r
+\r
+/**\r
+ * The Java Matrix Class provides the fundamental operations of numerical linear algebra.\r
+ * Original authors The MathWorks, Inc. and the National Institute of Standards and Technology\r
+ * The original public domain code has now been modified and reduced to only contain\r
+ * the use of QR Decomposition of rectangular matrices, to solve least squares regression,\r
+ * and is placed under GPL2 with the rest of the GpsPrune code.\r
+ */\r
+public class Matrix\r
+{\r
+\r
+       /** Array for internal storage of elements */\r
+       private double[][] _matrix;\r
+\r
+       /** Row and column dimensions */\r
+       private int _m, _n;\r
+\r
+\r
+       /**\r
+        * Construct an m-by-n matrix of zeros\r
+        * @param inM  Number of rows\r
+        * @param inN  Number of colums\r
+        */\r
+       public Matrix(int inM, int inN)\r
+       {\r
+               _m = inM;\r
+               _n = inN;\r
+               _matrix = new double[inM][inN];\r
+       }\r
+\r
+       /**\r
+        * Construct a matrix from a 2-D array\r
+        * @param A   Two-dimensional array of doubles.\r
+        * @exception IllegalArgumentException All rows must have the same length\r
+        */\r
+       public Matrix(double[][] A)\r
+       {\r
+               _m = A.length;\r
+               _n = A[0].length;\r
+               for (int i = 0; i < _m; i++) {\r
+                       if (A[i].length != _n) {\r
+                               throw new IllegalArgumentException("All rows must have the same length.");\r
+                       }\r
+               }\r
+               _matrix = A;\r
+       }\r
+\r
+       /**\r
+        * Construct a matrix quickly without checking arguments.\r
+        * @param inA   Two-dimensional array of doubles.\r
+        * @param inM   Number of rows\r
+        * @param inN   Number of columns\r
+        */\r
+       public Matrix(double[][] inA, int inM, int inN)\r
+       {\r
+               _matrix = inA;\r
+               _m = inM;\r
+               _n = inN;\r
+       }\r
+\r
+       /*\r
+        * ------------------------ Public Methods ------------------------\r
+        */\r
+\r
+\r
+       /**\r
+        * Set a value in a cell of the matrix\r
+        * @param inRow row index\r
+        * @param inCol column index\r
+        * @param inValue value to set\r
+        */\r
+       public void setValue(int inRow, int inCol, double inValue)\r
+       {\r
+               _matrix[inRow][inCol] = inValue;\r
+       }\r
+\r
+       /**\r
+        * Access the internal two-dimensional array.\r
+        * @return Pointer to the two-dimensional array of matrix elements.\r
+        */\r
+       public double[][] getArray() {\r
+               return _matrix;\r
+       }\r
+\r
+       /**\r
+        * Copy the internal two-dimensional array.\r
+        * @return Two-dimensional array copy of matrix elements.\r
+        */\r
+       public double[][] getArrayCopy()\r
+       {\r
+               double[][] C = new double[_m][_n];\r
+               for (int i = 0; i < _m; i++) {\r
+                       for (int j = 0; j < _n; j++) {\r
+                               C[i][j] = _matrix[i][j];\r
+                       }\r
+               }\r
+               return C;\r
+       }\r
+\r
+       /**\r
+        * Get a single element.\r
+        * @param inRow   Row index\r
+        * @param inCol   Column index\r
+        * @return A(inRow,inCol)\r
+        * @exception ArrayIndexOutOfBoundsException\r
+        */\r
+       public double get(int inRow, int inCol) {\r
+               return _matrix[inRow][inCol];\r
+       }\r
+\r
+       /** @return number of rows _m */\r
+       public int getNumRows() {\r
+               return _m;\r
+       }\r
+\r
+       /** @return number of columns _n */\r
+       public int getNumColumns() {\r
+               return _n;\r
+       }\r
+\r
+       /**\r
+        * Get a submatrix\r
+        * @param i0  Initial row index\r
+        * @param i1  Final row index\r
+        * @param j0  Initial column index\r
+        * @param j1  Final column index\r
+        * @return A(i0:i1,j0:j1)\r
+        * @exception ArrayIndexOutOfBoundsException\r
+        */\r
+       public Matrix getMatrix(int i0, int i1, int j0, int j1)\r
+       {\r
+               Matrix X = new Matrix(i1 - i0 + 1, j1 - j0 + 1);\r
+               double[][] B = X.getArray();\r
+               try {\r
+                       for (int i = i0; i <= i1; i++) {\r
+                               for (int j = j0; j <= j1; j++) {\r
+                                       B[i - i0][j - j0] = _matrix[i][j];\r
+                               }\r
+                       }\r
+               } catch (ArrayIndexOutOfBoundsException e) {\r
+                       throw new ArrayIndexOutOfBoundsException("Submatrix indices");\r
+               }\r
+               return X;\r
+       }\r
+\r
+\r
+       /**\r
+        * Linear algebraic matrix multiplication, A * B\r
+        * @param B   another matrix\r
+        * @return Matrix product, A * B\r
+        * @exception IllegalArgumentException if matrix dimensions don't agree\r
+        */\r
+       public Matrix times(Matrix B)\r
+       {\r
+               if (B._m != _n) {\r
+                       throw new IllegalArgumentException("Matrix inner dimensions must agree.");\r
+               }\r
+               Matrix X = new Matrix(_m, B._n);\r
+               double[][] C = X.getArray();\r
+               double[] Bcolj = new double[_n];\r
+               for (int j = 0; j < B._n; j++) {\r
+                       for (int k = 0; k < _n; k++) {\r
+                               Bcolj[k] = B._matrix[k][j];\r
+                       }\r
+                       for (int i = 0; i < _m; i++) {\r
+                               double[] Arowi = _matrix[i];\r
+                               double s = 0;\r
+                               for (int k = 0; k < _n; k++) {\r
+                                       s += Arowi[k] * Bcolj[k];\r
+                               }\r
+                               C[i][j] = s;\r
+                       }\r
+               }\r
+               return X;\r
+       }\r
+\r
+       /**\r
+        * Subtract the other matrix from this one\r
+        * @param B   another matrix\r
+        * @return difference this - B\r
+        * @exception IllegalArgumentException if matrix dimensions don't agree\r
+        */\r
+       public Matrix minus(Matrix B)\r
+       {\r
+               if (B._m != _m || B._n != _n) {\r
+                       throw new IllegalArgumentException("Matrix dimensions must agree.");\r
+               }\r
+               Matrix result = new Matrix(_m, _n);\r
+               for (int i = 0; i < _m; i++) {\r
+                       for (int j = 0; j < _n; j++) {\r
+                               result.setValue(i, j, get(i, j) - B.get(i, j));\r
+                       }\r
+               }\r
+               return result;\r
+       }\r
+\r
+       /**\r
+        * Divide each element of this matrix by the corresponding element in the other one\r
+        * @param B   another matrix\r
+        * @return this[i,j]/other[i,j]\r
+        * @exception IllegalArgumentException if matrix dimensions don't agree\r
+        */\r
+       public Matrix divideEach(Matrix B)\r
+       {\r
+               if (B._m != _m || B._n != _n) {\r
+                       throw new IllegalArgumentException("Matrix dimensions must agree.");\r
+               }\r
+               Matrix result = new Matrix(_m, _n);\r
+               for (int i = 0; i < _m; i++) {\r
+                       for (int j = 0; j < _n; j++) {\r
+                               result.setValue(i, j, get(i, j) / B.get(i, j));\r
+                       }\r
+               }\r
+               return result;\r
+       }\r
+\r
+       /**\r
+        * Solve A*X = B\r
+        * @param B   right hand side\r
+        * @return least squares solution\r
+        */\r
+       public Matrix solve(Matrix B) {\r
+               return new QRDecomposition(this).solve(B);\r
+       }\r
+\r
+       /**\r
+        * @return the average absolute value of all the elements in the matrix\r
+        */\r
+       public double getAverageAbsValue()\r
+       {\r
+               double total = 0.0;\r
+               for (int i = 0; i < _m; i++) {\r
+                       for (int j = 0; j < _n; j++) {\r
+                               total += Math.abs(_matrix[i][j]);\r
+                       }\r
+               }\r
+               return total / _m / _n;\r
+       }\r
+\r
+       /**\r
+        * Primitive output for debugging\r
+        */\r
+       public String toString()\r
+       {\r
+               StringBuilder builder = new StringBuilder();\r
+               builder.append('(');\r
+               for (int i = 0; i < _m; i++) {\r
+                       builder.append('(');\r
+                       for (int j = 0; j < _n; j++) {\r
+                               builder.append((_matrix[i][j]));\r
+                               builder.append(", ");\r
+                       }\r
+                       builder.append(") ");\r
+               }\r
+               builder.append(')');\r
+               return builder.toString();\r
+       }\r
+}\r
diff --git a/tim/prune/function/estimate/jama/QRDecomposition.java b/tim/prune/function/estimate/jama/QRDecomposition.java
new file mode 100644 (file)
index 0000000..e8aa2b7
--- /dev/null
@@ -0,0 +1,219 @@
+package tim.prune.function.estimate.jama;\r
+\r
+/**\r
+ * QR Decomposition.\r
+ *\r
+ * For an m-by-n matrix A with m >= n, the QR decomposition is an m-by-n\r
+ * orthogonal matrix Q and an n-by-n upper triangular matrix R so that A = Q*R.\r
+ *\r
+ * The QR decomposition always exists, even if the matrix does not have full\r
+ * rank, so the constructor will never fail. The primary use of the QR\r
+ * decomposition is in the least squares solution of nonsquare systems of\r
+ * simultaneous linear equations. This will fail if isFullRank() returns false.\r
+ *\r
+ * Original authors The MathWorks, Inc. and the National Institute of Standards and Technology\r
+ * The original public domain code has now been modified and reduced,\r
+ * and is placed under GPL2 with the rest of the GpsPrune code.\r
+ */\r
+public class QRDecomposition\r
+{\r
+\r
+       /** Array for internal storage of decomposition */\r
+       private double[][] _QR;\r
+\r
+       /** Row and column dimensions */\r
+       private int _m, _n;\r
+\r
+       /** Array for internal storage of diagonal of R */\r
+       private double[] _Rdiag;\r
+\r
+\r
+       /**\r
+        * QR Decomposition, computed by Householder reflections.\r
+        *\r
+        * @param inA   Rectangular matrix\r
+        * @return Structure to access R and the Householder vectors and compute Q.\r
+        */\r
+       public QRDecomposition(Matrix inA)\r
+       {\r
+               // Initialize.\r
+               _QR = inA.getArrayCopy();\r
+               _m = inA.getNumRows();\r
+               _n = inA.getNumColumns();\r
+               _Rdiag = new double[_n];\r
+\r
+               // Main loop.\r
+               for (int k = 0; k < _n; k++)\r
+               {\r
+                       // Compute 2-norm of k-th column without under/overflow.\r
+                       double nrm = 0;\r
+                       for (int i = k; i < _m; i++) {\r
+                               nrm = Maths.pythag(nrm, _QR[i][k]);\r
+                       }\r
+\r
+                       if (nrm != 0.0)\r
+                       {\r
+                               // Form k-th Householder vector.\r
+                               if (_QR[k][k] < 0) {\r
+                                       nrm = -nrm;\r
+                               }\r
+                               for (int i = k; i < _m; i++) {\r
+                                       _QR[i][k] /= nrm;\r
+                               }\r
+                               _QR[k][k] += 1.0;\r
+\r
+                               // Apply transformation to remaining columns.\r
+                               for (int j = k + 1; j < _n; j++)\r
+                               {\r
+                                       double s = 0.0;\r
+                                       for (int i = k; i < _m; i++) {\r
+                                               s += _QR[i][k] * _QR[i][j];\r
+                                       }\r
+                                       s = -s / _QR[k][k];\r
+                                       for (int i = k; i < _m; i++) {\r
+                                               _QR[i][j] += s * _QR[i][k];\r
+                                       }\r
+                               }\r
+                       }\r
+                       _Rdiag[k] = -nrm;\r
+               }\r
+       }\r
+\r
+       /*\r
+        * ------------------------ Public Methods ------------------------\r
+        */\r
+\r
+       /**\r
+        * Is the matrix full rank?\r
+        * @return true if R, and hence A, has full rank.\r
+        */\r
+       public boolean isFullRank()\r
+       {\r
+               for (int j = 0; j < _n; j++) {\r
+                       if (_Rdiag[j] == 0)\r
+                               return false;\r
+               }\r
+               return true;\r
+       }\r
+\r
+       /**\r
+        * Return the Householder vectors\r
+        * @deprecated\r
+        * @return Lower trapezoidal matrix whose columns define the reflections\r
+        */\r
+       private Matrix getH()\r
+       {\r
+               Matrix X = new Matrix(_m, _n);\r
+               double[][] H = X.getArray();\r
+               for (int i = 0; i < _m; i++) {\r
+                       for (int j = 0; j < _n; j++) {\r
+                               if (i >= j) {\r
+                                       H[i][j] = _QR[i][j];\r
+                               } else {\r
+                                       H[i][j] = 0.0;\r
+                               }\r
+                       }\r
+               }\r
+               return X;\r
+       }\r
+\r
+       /**\r
+        * Return the upper triangular factor\r
+        * @deprecated\r
+        * @return R\r
+        */\r
+       private Matrix getR()\r
+       {\r
+               Matrix X = new Matrix(_n, _n);\r
+               double[][] R = X.getArray();\r
+               for (int i = 0; i < _n; i++) {\r
+                       for (int j = 0; j < _n; j++) {\r
+                               if (i < j) {\r
+                                       R[i][j] = _QR[i][j];\r
+                               } else if (i == j) {\r
+                                       R[i][j] = _Rdiag[i];\r
+                               } else {\r
+                                       R[i][j] = 0.0;\r
+                               }\r
+                       }\r
+               }\r
+               return X;\r
+       }\r
+\r
+       /**\r
+        * Generate and return the (economy-sized) orthogonal factor\r
+        * @deprecated\r
+        * @return Q\r
+        */\r
+       private Matrix getQ()\r
+       {\r
+               Matrix X = new Matrix(_m, _n);\r
+               double[][] Q = X.getArray();\r
+               for (int k = _n - 1; k >= 0; k--) {\r
+                       for (int i = 0; i < _m; i++) {\r
+                               Q[i][k] = 0.0;\r
+                       }\r
+                       Q[k][k] = 1.0;\r
+                       for (int j = k; j < _n; j++) {\r
+                               if (_QR[k][k] != 0) {\r
+                                       double s = 0.0;\r
+                                       for (int i = k; i < _m; i++) {\r
+                                               s += _QR[i][k] * Q[i][j];\r
+                                       }\r
+                                       s = -s / _QR[k][k];\r
+                                       for (int i = k; i < _m; i++) {\r
+                                               Q[i][j] += s * _QR[i][k];\r
+                                       }\r
+                               }\r
+                       }\r
+               }\r
+               return X;\r
+       }\r
+\r
+       /**\r
+        * Least squares solution of A*X = B\r
+        * @param B   A Matrix with as many rows as A and any number of columns\r
+        * @return X that minimizes the two norm of Q*R*X-B\r
+        * @exception IllegalArgumentException if matrix dimensions don't agree\r
+        * @exception RuntimeException         if Matrix is rank deficient.\r
+        */\r
+       public Matrix solve(Matrix B)\r
+       {\r
+               if (B.getNumRows() != _m) {\r
+                       throw new IllegalArgumentException("Matrix row dimensions must agree.");\r
+               }\r
+               if (!isFullRank()) {\r
+                       throw new RuntimeException("Matrix is rank deficient.");\r
+               }\r
+\r
+               // Copy right hand side\r
+               int nx = B.getNumColumns();\r
+               double[][] X = B.getArrayCopy();\r
+\r
+               // Compute Y = transpose(Q)*B\r
+               for (int k = 0; k < _n; k++) {\r
+                       for (int j = 0; j < nx; j++) {\r
+                               double s = 0.0;\r
+                               for (int i = k; i < _m; i++) {\r
+                                       s += _QR[i][k] * X[i][j];\r
+                               }\r
+                               s = -s / _QR[k][k];\r
+                               for (int i = k; i < _m; i++) {\r
+                                       X[i][j] += s * _QR[i][k];\r
+                               }\r
+                       }\r
+               }\r
+               // Solve R*X = Y;\r
+               for (int k = _n - 1; k >= 0; k--) {\r
+                       for (int j = 0; j < nx; j++) {\r
+                               X[k][j] /= _Rdiag[k];\r
+                       }\r
+                       for (int i = 0; i < k; i++) {\r
+                               for (int j = 0; j < nx; j++) {\r
+                                       X[i][j] -= X[k][j] * _QR[i][k];\r
+                               }\r
+                       }\r
+               }\r
+               return (new Matrix(X, _n, nx).getMatrix(0, _n - 1, 0, nx - 1));\r
+       }\r
+}\r
index cf634c0fbc6b48b932382d21b3695b9e30ddc575..286ba1aae7bc677cd835e3855ea213d44e206b6d 100644 (file)
@@ -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();
 
index 904dd8bc62248d49fe4e5669ee37fccc19a643a3..274c0b882a3bf51f1ee7c7dfe278646f756ff235 100644 (file)
@@ -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
index 586cbb6d72eede3c5a62d77b68c8649b8845c5d8..9a4ae30b7d4aa3d5c2506dd5c496d86a2485fad3 100644 (file)
@@ -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<numActivities; i++)
index 22ee8311a11c91222f58475dfdd2df6e0f750ca2..4d13348d3c4be742656f3b3690c7a9db09609ca8 100644 (file)
@@ -1,23 +1,12 @@
 package tim.prune.function.srtm;
 
-import java.awt.BorderLayout;
-import java.awt.Component;
-import java.awt.Dimension;
-import java.awt.FlowLayout;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
 import java.io.IOException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 
-import javax.swing.JButton;
-import javax.swing.JDialog;
-import javax.swing.JLabel;
 import javax.swing.JOptionPane;
-import javax.swing.JPanel;
-import javax.swing.JProgressBar;
 
 import tim.prune.App;
 import tim.prune.DataSubscriber;
@@ -27,6 +16,7 @@ import tim.prune.UpdateMessageBroker;
 import tim.prune.data.DataPoint;
 import tim.prune.data.Field;
 import tim.prune.data.Track;
+import tim.prune.gui.ProgressDialog;
 import tim.prune.undo.UndoLookupSrtm;
 
 /**
@@ -37,12 +27,8 @@ import tim.prune.undo.UndoLookupSrtm;
  */
 public class LookupSrtmFunction extends GenericFunction implements Runnable
 {
-       /** function dialog */
-       private JDialog _dialog = null;
-       /** Progress bar for function */
-       private JProgressBar _progressBar = null;
-       /** Cancel flag */
-       private boolean _cancelled = false;
+       /** Progress dialog */
+       ProgressDialog _progress = null;
 
        /** Expected size of hgt file in bytes */
        private static final long HGT_SIZE = 2884802L;
@@ -69,49 +55,16 @@ public class LookupSrtmFunction extends GenericFunction implements Runnable
         */
        public void begin()
        {
-               if (_dialog == null)
+               if (_progress == null)
                {
-                       _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), false);
-                       _dialog.setLocationRelativeTo(_parentFrame);
-                       _dialog.getContentPane().add(makeDialogComponents());
-                       _dialog.pack();
+                       _progress = new ProgressDialog(_parentFrame, getNameKey());
                }
-               _progressBar.setMinimum(0);
-               _progressBar.setMaximum(100);
-               _progressBar.setValue(20);
-               _cancelled = false;
+               _progress.show();
                // start new thread for time-consuming part
                new Thread(this).start();
        }
 
 
-       /**
-        * 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;
-       }
-
        /**
         * Run method using separate thread
         */
@@ -145,7 +98,6 @@ public class LookupSrtmFunction extends GenericFunction implements Runnable
                        overwriteZeros = true;
                }
 
-               _dialog.setVisible(true);
                // Now loop again to extract the required tiles
                for (int i=0; i<track.getNumPoints(); i++)
                {
@@ -176,20 +128,19 @@ public class LookupSrtmFunction extends GenericFunction implements Runnable
                UndoLookupSrtm undo = new UndoLookupSrtm(_app.getTrackInfo());
                int numAltitudesFound = 0;
                // Update progress bar
-               _progressBar.setMaximum(inTileList.size());
-               _progressBar.setIndeterminate(inTileList.size() <= 1);
-               _progressBar.setValue(0);
+               _progress.setMaximum(inTileList.size());
+               _progress.setValue(0);
                String errorMessage = null;
                // Get urls for each tile
                URL[] urls = TileFinder.getUrls(inTileList);
-               for (int t=0; t<inTileList.size() && !_cancelled; t++)
+               for (int t=0; t<inTileList.size() && !_progress.isCancelled(); t++)
                {
                        if (urls[t] != null)
                        {
                                SrtmTile tile = inTileList.get(t);
                                try
                                {
-                                       _progressBar.setValue(t);
+                                       _progress.setValue(t);
                                        final int ARRLENGTH = 1201*1201;
                                        int[] heights = new int[ARRLENGTH];
                                        // Open zipinputstream on url and check size
@@ -253,8 +204,8 @@ public class LookupSrtmFunction extends GenericFunction implements Runnable
                                }
                        }
                }
-               _dialog.dispose();
-               if (_cancelled) {return;}
+               _progress.dispose();
+               if (_progress.isCancelled()) {return;}
                if (numAltitudesFound > 0)
                {
                        // Inform app including undo information
index 209db7f58d439208caa363097236f40805957640..afcc28f274a2634b3bb74de613fa4cb653630a92 100644 (file)
@@ -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 (file)
index 0000000..f1baa2a
--- /dev/null
@@ -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;
+       }
+}
index 7b3a4ff6843f9b34c4c7df048081a54f92ec17e6..13df7cd31609b0ea8ef1eb6b7fdb59d6639a722d 100644 (file)
@@ -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) + "...";
        }
 }
index df706b74e17c754661ebc164b22a8adf2724b56c..49ca3870b6304ccc3bc2c40f56b18d2b3685a6fe 100644 (file)
@@ -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);
+       }
 }
index 7b226b5f9af0afc48d1f18384f27d2b859bcd6f8..703f7dd1d1be00f6cca07525fe54ae28e9db125f 100644 (file)
@@ -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++;
+       }
 }
index 72537b4098716d08c2a23a4e61f62465ed3c620f..799e90f76b0c8bc2faf221ec12001fca0b0012b6 100644 (file)
@@ -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)
index c5501eee75dd653b3a72685bdd71867e14df7cc4..16a0a66a73d7c84792063a4054dcaec256b78598 100644 (file)
@@ -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 (file)
index 0000000..d09087e
--- /dev/null
@@ -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 (file)
index 0000000..26adb1b
--- /dev/null
@@ -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;
+               }
+       }
+}
index 09f676d7e90caac0c119922b3b0bcf894798c9cc..ba953fc67c02381bf246616c21bbacf19fe5b420 100644 (file)
@@ -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 (file)
index 0000000..1a5a8f1
--- /dev/null
@@ -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;
+               }
+       }
+}
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
diff --git a/tim/prune/gui/images/entry_invalid.gif b/tim/prune/gui/images/entry_invalid.gif
new file mode 100644 (file)
index 0000000..8612eaf
Binary files /dev/null and b/tim/prune/gui/images/entry_invalid.gif differ
diff --git a/tim/prune/gui/images/entry_none.gif b/tim/prune/gui/images/entry_none.gif
new file mode 100644 (file)
index 0000000..720eeec
Binary files /dev/null and b/tim/prune/gui/images/entry_none.gif differ
diff --git a/tim/prune/gui/images/entry_valid.gif b/tim/prune/gui/images/entry_valid.gif
new file mode 100644 (file)
index 0000000..9ca8275
Binary files /dev/null and b/tim/prune/gui/images/entry_valid.gif differ
index 96ff22367e89d5137e1710c96e104a2911ff80de..a6d5a8861fb62600b9e006f36b29d7466d9dd456 100644 (file)
@@ -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<inWidth; x++)
-                       {
-                               // loop over y coordinate of rectangle
-                               for (int y=0; y<inHeight; y++)
-                               {
-                                       int pixelColor = _mapImage.getRGB(inX + x, inY - y);
-                                       // split into four components rgba
-                                       int pixLow = pixelColor & 255;
-                                       int pixMid = (pixelColor >> 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();
        }
index 78206a3956959f6126ed1568a29893a61ce76e36..49d94676f77c70cc137fa1d204a11dffcd4eea27 100644 (file)
@@ -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<inWidth; x++)
+                       {
+                               // loop over y coordinate of rectangle
+                               for (int y=0; y<inHeight; y++)
+                               {
+                                       int pixelColor = inImage.getRGB(inX + x, inY - y);
+                                       // split into four components rgba
+                                       int pixLow = pixelColor & 255;
+                                       int pixMid = (pixelColor >> 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;
+       }
 }
index 9c19862166b2421b4cdaab9d102a75b5eb26dad6..9600b1674ade17a296a9e43f9a1dc4039dfcffe1 100644 (file)
@@ -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 (file)
index 0000000..cf974a5
--- /dev/null
@@ -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";
+       }
+}
index 770a211ea777535d900bb43bfed109c047b771a5..732669a956dcd48bf719848e5223b2b77bf989fe 100644 (file)
@@ -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<fields.getNumFields(); i++)
+               {
+                       Field field = fields.getField(i);
+                       if (!field.isBuiltIn())
+                       {
+                               if (addSeparator) {_popup.addSeparator();}
+                               addSeparator = false;
+                               JMenuItem item = new JMenuItem(field.getName());
+                               item.addActionListener(new MenuClicker(field));
+                               _popup.add(item);
+                       }
+               }
        }
 
        /**
@@ -309,6 +335,10 @@ public class ProfileChart extends GenericDisplay implements MouseListener
                if (inUpdateType != SELECTION_CHANGED) {
                        _data.init(Config.getUnitSet());
                }
+               // Update the menu if necessary
+               if ((inUpdateType & DATA_ADDED_OR_REMOVED) > 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();
index 2d0e54afef2a9dd2de1f4ad3e887e20841b2fa28..09d72deab611db074b3f26b6710bbbd3a319781c 100644 (file)
@@ -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
index 6c5a9dbc6578cf2d78d74b7b654efae96d6e4def..b922f3bcbbb8b85ff3e29da18a14b3286dc27db7 100644 (file)
@@ -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=<table><tr><td>\u0160ipky</td><td>Posunout mapu vlevo, vpravo, nahoru, dol\u016f</td></tr><tr><td>Ctrl + \u0161ipka vlevo, vpravo</td><td>Vybrat p\u0159edchoz\u00ed nebo n\u00e1sleduj\u00edc\u00ed bod</td></tr><tr><td>Ctrl + \u0161ipka nahoru, dol\u016f</td><td>P\u0159ibl\u00ed\u017eit, odd\u00e1lit</td></tr><tr><td>Ctrl + PgUp, PgDown</td><td>Vybrat p\u0159edchoz\u00ed, n\u00e1sleduj\u00edc\u00ed \u010d\u00e1st trasy</td></tr><tr><td>Ctrl + Home, End</td><td>Vybrat prvn\u00ed, posledn\u00ed bod</td></tr><tr><td>Del</td><td>Smazat aktu\u00e1ln\u00ed bod</td></tr></table>
+dialog.keys.keylist=<table><tr><td>\u0160ipky</td><td>Posunout mapu vlevo, vpravo, nahoru, dol\u016f</td></tr><tr><td>Ctrl + \u0161ipka vlevo, vpravo</td><td>Vybrat p\u0159edchoz\u00ed nebo n\u00e1sleduj\u00edc\u00ed bod</td></tr><tr><td>Ctrl + \u0161ipka nahoru, dol\u016f</td><td>P\u0159ibl\u00ed\u017eit, odd\u00e1lit</td></tr><tr><td>Ctrl + PgUp, PgDown</td><td>Vybrat p\u0159edchoz\u00ed, n\u00e1sleduj\u00edc\u00ed \u010d\u00e1st stopy</td></tr><tr><td>Ctrl + Home, End</td><td>Vybrat prvn\u00ed, posledn\u00ed bod</td></tr><tr><td>Del</td><td>Smazat aktu\u00e1ln\u00ed bod</td></tr></table>
 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.
index b375fe34daca8e58ba5ce55b6788c66c2b863413..ea394b97388e35d75b68355379b12ab6d4e8782c 100644 (file)
@@ -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
index ee57c2152143d5d110b10b8d5e97bd7f3cddb192..4914e44ae0546c3c235f0e2240b86bead6c0a090 100644 (file)
@@ -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.
index caa5bd6a17faadb83a67042c2f89dbdaa999929a..fa2931f6e38357b47e6f04e3c755d4cc115af185 100644 (file)
@@ -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.
index f2e70e1d9a6ba94f8a8c7d92dafa36985d9e3426..0193cb8c03f8d182dde38c64611705559ededa66 100644 (file)
@@ -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.
index b2b2fc275285a3fe1c3bcb49e8b265d7cb4e4aa6..928d027dbc5180d6bd9b9ced150ed53aa0544eaf 100644 (file)
@@ -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
index 77f889328b5b00cbab166c52346afb72f1873181..2d2f3a5adf027984b8b34cfdc1119d9a52be5434 100644 (file)
@@ -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
index ab11b33f6089961693ed41c55e7ccb1b60625894..83045ea704db224eb00e9c9e877fbedd6baa423b 100644 (file)
@@ -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
index 837f9d9decf02318d9d028089261b6e4612f684b..0764c74b578afbe683c83dc01ebbd0c43c3e948b 100644 (file)
@@ -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
index 989110a6b4820a0fce93565e81bb02004b86aa96..07027a00f1bb31d6d822b16e4ead7c6d95efa83e 100644 (file)
@@ -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
index 33b54c9f3679a49d41394b5a7d8c1309dced5592..9739f87cf220e8d157e32de8e55d5bd9592cb612 100644 (file)
@@ -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
index 68c5cd7910fe837cbfdba48a039cb9a7c9c6b6ea..b2b45964b3f3bdf261181f199ec861cb0bf12a08 100644 (file)
@@ -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
index 5a75c2aac89438ac98a6d2f8bcea647f4a44a4d9..12c9307fa4a5f485678a0c7c2bb4ec90086ac299 100644 (file)
@@ -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.
index 2ff8980250c2d5c598dffccdc1453ad91ed32fd9..7972f5940b8cadbac90aaa0a259bc6bc5abd3c48 100644 (file)
@@ -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
index 1220049fcfcdf17c7a8abe761f9e88c648ddac25..1d0e3efdc75a9ed5aac69727c4209a517ac5a4be 100644 (file)
@@ -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
index c169538cc89af7250073129440ae76a7bd276bbd..fc851289a35f7ad4403d9451a9483ec5c8c69aea 100644 (file)
@@ -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
index d0983667dbcb4621f7a8bb4613fa1fd8a44f888a..da661a9f8e37bd9136cb7e31547e661cf4a7384a 100644 (file)
@@ -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
index 6068744d6f902cc5d8cb29c63142c3cb9120effd..d1936918b7b8fdd5f5d14d177e798d59cffcbd21 100644 (file)
@@ -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=<table><tr><td>\u7bad\u5934</td><td>\u4e0a\u4e0b\u5de6\u53f3\u79fb\u52a8\u5730\u56fe</td></tr><tr><td>Ctrl + \u5de6\u53f3\u7bad\u5934</td><td>\u9009\u53d6\u524d\uff0c\u540e\u70b9</td></tr><tr><td>Ctrl + \u4e0a\u4e0b\u7bad\u5934</td><td>\u653e\u5927\u7f29\u5c0f</td></tr><tr><td>Ctrl + PgUp, PgDown</td><td>\u9009\u62e9\u524d\u540e\u6bb5</td></tr><tr><td>Ctrl + Home, End</td><td>\u9009\u62e9\u9996\u672b\u70b9</td></tr><tr><td>Del</td><td>\u5220\u9664\u5f53\u524d\u70b9</td></tr></table>
 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
index ed7e627f424ea2794d51641d1ef791cca4b1481a..2d1c9e80098ee85d29250b68b36b5c075389a3bc 100644 (file)
@@ -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,
index f7866efaf0cacc1512dbca0facc3b2aa509df944..1897e0cdcf9d717e442ffe2522c43223a2d466fb 100644 (file)
@@ -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);
+               }
        }
 }
index 8c4b4dd17c2170121c8e9f2cf7e28d3cb2b2e1ce..130ca793dd30bc118fb637fa485971619ab73241 100644 (file)
@@ -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);
        }
 }
index b970665c0c36d3f19de4d3d3583d4da9b1ce1c2a..7ebd7deadc73a50023cb5baecc76936296a3eae9 100644 (file)
@@ -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<String> commandList = new ArrayList<String>();
+               // 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 (file)
index 0000000..45eeccd
--- /dev/null
@@ -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<ComponentPair> _componentList = new ArrayList<ComponentPair>(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);
+                       }
+               }
+       }
+}
index 9b2bdc787a9c9a0833d7d5efd08380535b5dcc21..c76318d16bfb0be060e315a52a2a35a9eeade06e 100644 (file)
@@ -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<inValues.length; v++)
+                       for (String value : inValues)
                        {
-                               Latitude lat = new Latitude(inValues[v]);
-                               if (lat.isValid()) {return false;}
+                               if (fieldLooksNumeric(value)) {return false;}
                        }
                }
-               // No valid Latitude value found so presume header
+               // No (mostly) numeric values found so presume header
                return true;
        }
 
 
+       /**
+        * See if a field looks numeric or not by comparing the number of numeric vs non-numeric characters
+        * @param inValue field value to check
+        * @return true if there are more numeric characters than not
+        */
+       private static boolean fieldLooksNumeric(String inValue)
+       {
+               if (inValue == null) {
+                       return false;
+               }
+               final int numChars = inValue.length();
+               if (numChars < 3) {return false;} // Don't care about one or two character values
+               // Loop through characters seeing which ones are numeric and which not
+               int numNums = 0;
+               for (int i=0; i<numChars; i++)
+               {
+                       char currChar = inValue.charAt(i);
+                       if (currChar >= '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;
index 1d12397dd61514a904faae91b0d32902c341afe2..da354aac8b253895324e84b8a2b67d0ed74bbcb3 100644 (file)
@@ -42,8 +42,14 @@ public class FileCacher
                        {
                                reader = new BufferedReader(new FileReader(_file));
                                String currLine = reader.readLine();
+                               if (currLine != null && currLine.startsWith("<?xml")) {
+                                       return; // it's an xml file, it shouldn't use this cacher
+                               }
                                while (currLine != null)
                                {
+                                       if (currLine.indexOf('\0') >= 0) {
+                                               return; // it's a binary file, shouldn't use this cacher
+                                       }
                                        if (currLine.trim().length() > 0)
                                                contentList.add(currLine);
                                        currLine = reader.readLine();
index 5672c69fa9cc6052e4b303efb2ad28a252068cf0..8669e189c94043b4e3ab91889c4acf4b5e52dc0e 100644 (file)
@@ -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<Photo> photoSet = new TreeSet<Photo>();
+                       photoSet.add(photo);
+                       _app.informPhotosLoaded(photoSet);
+                       _app.informNoDataLoaded(); // To trigger load of next file if any
+               }
                else
                {
                        // Use text loader for everything else
index 8a376d8e6367b81aff9a9fca9f2c1f4edd1bafaa..a177eaad9f77455efa2f4ba51ac66d3eb0ce3660 100644 (file)
@@ -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);
        }
index 4ed2ab0f213d64f89be89520e3f1b89f923a0840..a052b55d14fca6dabc18497775575005945410bd 100644 (file)
@@ -94,7 +94,6 @@ public class MediaLoadProgressDialog
                        _progressBar.setMaximum(inMax);
                _progressBar.setValue(inCurrent);
                _progressBar.setString("" + inCurrent + " / " + _progressBar.getMaximum());
-               // TODO: Need to repaint?
        }
 
        /**
index 2dd63dec16088fa4350b5b914c2c25a8d59ccdbc..d62ba5fe1660a08a3398004b5b9e44b710f3d1c4 100644 (file)
@@ -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);
                }
        }
 
index 1f88012807485d8010f387947bb00d9621375a03..53d6d2a77d50de6355be51ff0f19af1808b9d808 100644 (file)
@@ -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<inFields.length; i++) {
+                               if (inFields[i] == inCheck) { // == check ok here because it only checks for built-in fields
+                                       return true;
+                               }
+                       }
+               }
+               return false;
+       }
 
        /**
         * All options have been selected, so load file
@@ -529,22 +630,39 @@ public class TextFileLoader
                // Save delimiter, field array and altitude format for later use
                _lastUsedDelimiter = _currentDelimiter;
                _lastSelectedFields = _fieldTableModel.getFieldArray();
-               Altitude.Format altitudeFormat = Altitude.Format.METRES;
-               if (_unitsDropDown.getSelectedIndex() == 1)
-               {
-                       altitudeFormat = Altitude.Format.FEET;
-               }
-               _lastAltitudeFormat = altitudeFormat;
-               // give data to App
+               // TODO: Remember all the units selections for next load?
+               // Get the selected units for altitudes and speeds
                SourceInfo sourceInfo = new SourceInfo(_file, SourceInfo.FILE_TYPE.TEXT);
+               PointCreateOptions options = new PointCreateOptions();
+               options.setAltitudeUnits(_altitudeUnitsDropdown.getSelectedIndex() == 0 ? UnitSetLibrary.UNITS_METRES : UnitSetLibrary.UNITS_FEET);
+               Unit hSpeedUnit = UnitSetLibrary.ALL_SPEED_UNITS[_hSpeedUnitsDropdown.getSelectedIndex()];
+               options.setSpeedUnits(hSpeedUnit);
+               Unit vSpeedUnit = UnitSetLibrary.ALL_SPEED_UNITS[_vSpeedUnitsDropdown.getSelectedIndex()];
+               options.setVerticalSpeedUnits(vSpeedUnit, _vSpeedUpwardsRadio.isSelected());
+
+               // give data to App
                _app.informDataLoaded(_fieldTableModel.getFieldArray(),
-                       _fileExtractTableModel.getData(), altitudeFormat, sourceInfo, null);
+                       _fileExtractTableModel.getData(), options, sourceInfo, null);
                // clear up file cacher
                _fileCacher.clear();
                // dispose of dialog
                _dialog.dispose();
        }
 
+       /**
+        * @return true if the inputs on the current tab are valid, user is allowed to proceed
+        */
+       private boolean isCurrentCardValid()
+       {
+               int cardIndex = _wizard.getCurrentCardIndex();
+               if (cardIndex == 1)
+               {
+                       // validate second panel
+                       return _fieldTableModel.getRowCount() > 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 (file)
index 0000000..7de9a3a
--- /dev/null
@@ -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 (file)
index 0000000..b5b564f
--- /dev/null
@@ -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 (file)
index 0000000..f310031
--- /dev/null
@@ -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 (file)
index 0000000..7554160
--- /dev/null
@@ -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 (file)
index 0000000..8417908
--- /dev/null
@@ -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 (file)
index 0000000..a169846
--- /dev/null
@@ -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 (file)
index 0000000..ebbc9f4
--- /dev/null
@@ -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();
+       }
+}
index 98ef9467943645182d8f6a78d9865e50ab51ba6d..3ebd4caae7658eca9e345ada1c25d202ef4fde53 100644 (file)
@@ -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()));
                        }
                }
index 4c4239d6b2081d2ca2799d6d38a6fe8136b659e1..783ac798ed3d0bf9a6a580cb1ee33b4cc1ebfb18 100644 (file)
@@ -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()));
                        }
                }
index fff0d92aa67f1587aa6e311411bc1f43caecd293..74e48f9497c6ab387dd779b2a97747511e320a85 100644 (file)
@@ -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;
                                                }
index 6cddea5915cbc8ebe49fd70a3faecb1131f37f63..f5379c366b93c8fe86c262696516146202a11098 100644 (file)
@@ -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 (file)
index 0000000..514ebd7
--- /dev/null
@@ -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<MapSourceLibrary.getNumSources(); i++)
+               {
+                       _mapSourceDropdown.addItem(MapSourceLibrary.getSource(i).getName());
+               }
+               if (_sourceIndex < 0 || _sourceIndex >= _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();
+       }
+}
index 020ef537ff4fa3f936c6e23cc1032bd0abb360ca..3c1a7b814f605777054862d04632f3123ed05794 100644 (file)
@@ -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();
index 6be44eb80bc3eb6beb828f0270db15c75255954e..b4a73d43d86f301a464d8ab5dde1e68be189d332 100644 (file)
@@ -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) {}
                }
index 19007ac920dc33c45c2522def2adb648ea1368c7..8d9df8a0c15f8a10f79fc98a0368b609b841d617 100644 (file)
@@ -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, "<ele>", "</ele>", inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
+               source = replaceGpxTags(source, "<ele>", "</ele>", inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
                source = replaceGpxTags(source, "<time>", "</time>", 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<ele>");
-                       inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
+                       inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
                        inWriter.write("</ele>\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("<ele>");
-                       inWriter.write("" + inPoint.getAltitude().getStringValue(Altitude.Format.METRES));
+                       inWriter.write("" + inPoint.getAltitude().getStringValue(UnitSetLibrary.UNITS_METRES));
                        inWriter.write("</ele>");
                }
                // timestamp if available (and selected)
diff --git a/tim/prune/save/GroutedImage.java b/tim/prune/save/GroutedImage.java
new file mode 100644 (file)
index 0000000..4400067
--- /dev/null
@@ -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 (file)
index 0000000..fabed33
--- /dev/null
@@ -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<numPoints; i++)
+               {
+                       DataPoint point = track.getPoint(i);
+                       if (!point.isWaypoint())
+                       {
+                               double x = track.getX(i) - xRange.getMinimum();
+                               double y = track.getY(i) - yRange.getMinimum();
+                               // use zoom level to calculate pixel coords on image
+                               int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
+                               // System.out.println("Point: x=" + x + ", px=" + px + ", y=" + y + ", py=" + py);
+                               if (!point.getSegmentStart()) {
+                                       // draw from previous point to this one
+                                       g.drawLine(prevX, prevY, px, py);
+                               }
+                               // draw this point
+                               g.drawRect(px-2, py-2, 3, 3);
+                               // save coordinates
+                               prevX = px; prevY = py;
+                       }
+               }
+               // Draw waypoints
+               final Color textColour = Config.getColourScheme().getColour(ColourScheme.IDX_TEXT);
+               g.setColor(textColour);
+               // Loop over points
+               for (int i=0; i<numPoints; i++)
+               {
+                       DataPoint point = track.getPoint(i);
+                       if (point.isWaypoint())
+                       {
+                               // draw blob for each waypoint
+                               double x = track.getX(i) - xRange.getMinimum();
+                               double y = track.getY(i) - yRange.getMinimum();
+                               // use zoom level to calculate pixel coords on image
+                               int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
+                               g.fillRect(px-3, py-3, 6, 6);
+                       }
+               }
+               // Set text size according to input
+               int fontScalePercent = _textScaleField.getValue();
+               if (fontScalePercent > 10 && fontScalePercent <= 999)
+               {
+                       Font gFont = g.getFont();
+                       g.setFont(gFont.deriveFont((float) (gFont.getSize() * 0.01 * fontScalePercent)));
+               }
+               FontMetrics fm = g.getFontMetrics();
+               final int nameHeight = fm.getHeight();
+               final int imageSize = inImage.getImageSize();
+
+               // Loop over points again, draw photo points
+               final Color photoColour = Config.getColourScheme().getColour(ColourScheme.IDX_SECONDARY);
+               g.setColor(photoColour);
+               for (int i=0; i<numPoints; i++)
+               {
+                       DataPoint point = track.getPoint(i);
+                       if (point.hasMedia())
+                       {
+                               // draw blob for each photo
+                               double x = track.getX(i) - xRange.getMinimum();
+                               double y = track.getY(i) - yRange.getMinimum();
+                               // use zoom level to calculate pixel coords on image
+                               int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
+                               g.fillRect(px-3, py-3, 6, 6);
+                       }
+               }
+
+               // Loop over points again, now draw names for waypoints
+               g.setColor(textColour);
+               for (int i=0; i<numPoints; i++)
+               {
+                       DataPoint point = track.getPoint(i);
+                       if (point.isWaypoint())
+                       {
+                               double x = track.getX(i) - xRange.getMinimum();
+                               double y = track.getY(i) - yRange.getMinimum();
+                               int px = (int) (x * zoomFactor * 256), py = (int) (y * zoomFactor * 256);
+
+                               // Figure out where to draw waypoint name so it doesn't obscure track
+                               String waypointName = point.getWaypointName();
+                               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)
+                               {
+                                       // Shift arrays for coordinates right left up down
+                                       nameXs[0] += 2; nameXs[1] -= 2;
+                                       nameYs[2] -= 2; nameYs[3] += 2;
+                                       // Check each direction in turn right left up down
+                                       for (int a=0; a<4; a++)
+                                       {
+                                               if (nameXs[a] > 0 && (nameXs[a] + nameWidth) < imageSize
+                                                       && nameYs[a] < imageSize && (nameYs[a] - nameHeight) > 0
+                                                       && !MapUtils.overlapsPoints(inImage.getImage(), nameXs[a], nameYs[a],
+                                                               nameWidth, nameHeight, textColour))
+                                               {
+                                                       // Found a rectangle to fit - draw name here and quit
+                                                       g.drawString(waypointName, nameXs[a], nameYs[a]);
+                                                       drawnName = true;
+                                                       break;
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               // Maybe draw note at the bottom, export from GpsPrune?  Filename?
+               // Note: Differences from main map: No mapPosition (modifying position and visible points),
+               //       no selection, no opacities, maybe different scale/text factors
+       }
+}
diff --git a/tim/prune/save/ImagePreviewPanel.java b/tim/prune/save/ImagePreviewPanel.java
new file mode 100644 (file)
index 0000000..6f4ac88
--- /dev/null
@@ -0,0 +1,98 @@
+package tim.prune.save;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+
+import javax.swing.JPanel;
+
+import tim.prune.I18nManager;
+
+/**
+ * GUI component to display an image preview for the POV export
+ * and for the image export
+ */
+public class ImagePreviewPanel extends JPanel
+{
+       /** Base image */
+       private GroutedImage _baseImage = null;
+       /** Loading flag */
+       private boolean _loading = false;
+
+       /** String to show before image is loaded */
+       private static final String LOADING_STRING = I18nManager.getText("details.photo.loading") + " ...";
+       /** Colour to use if there aren't any tiles at all (same as colour of POV export without image) */
+       private static final Color EMPTY_IMAGE_COLOUR = new Color(0.5f, 0.75f, 0.8f);
+
+
+       /** Set the base image */
+       public void setImage(GroutedImage inImage)
+       {
+               _baseImage = inImage;
+               _loading = false;
+               repaint();
+       }
+
+       /** Inform that a load is starting */
+       public void startLoading()
+       {
+               _baseImage = null;
+               _loading = true;
+               repaint();
+       }
+
+       /** Get minimum size */
+       public Dimension getMinimumSize() {
+               return new Dimension(200, 200);
+       }
+       /** Preferred size */
+       public Dimension getPreferredSize() {
+               return getMinimumSize();
+       }
+
+       /**
+        * Override paint method
+        * @see javax.swing.JComponent#paint(java.awt.Graphics)
+        */
+       public void paint(Graphics inG)
+       {
+               super.paint(inG);
+               if (_loading)
+               {
+                       inG.setColor(Color.BLACK);
+                       inG.drawString(LOADING_STRING, 10, 30);
+               }
+               else if (_baseImage != null)
+               {
+                       final int width = getWidth();
+                       final int height = getHeight();
+                       final int previewSize = Math.min(width, height);
+                       if (previewSize > 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;
+       }
+}
index e1cc1516ac77e4e8c9da1967f748bd3ee78cd89c..6fbf7498bf4651e3445e00ae7f566703bc615cd1 100644 (file)
@@ -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<n>.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("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n<Folder>\n");
-               inWriter.write("\t<name>");
+               // Define xml header (depending on whether extensions are used or not)
+               if (useGxExtensions()) {
+                       inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.2\" xmlns:gx=\"http://www.google.com/kml/ext/2.2\">\n");
+               }
+               else {
+                       inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://earth.google.com/kml/2.1\">\n");
+               }
+               inWriter.write("<Folder>\n\t<name>");
                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<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
-                               + "\t\t\t\t<color>cc" + reverse(ColourUtils.makeHexCode(_colourPatch.getBackground())) + "</color>\n"
-                               + "\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
-                               + "\t\t\t<PolyStyle><color>33cc0000</color></PolyStyle>\n"
-                               + "\t\t</Style>\n\t\t<LineString>\n";
-                       if (absoluteAltitudes) {
-                               trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\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\t<altitudeMode>clampToGround</altitudeMode>\n";
+                               // Write track using standard KML
+                               numSaved += writeStandardTrack(inWriter, absoluteAltitudes, selStart, selEnd);
                        }
-                       trackStart += "\t\t\t<coordinates>";
-                       String trackEnd = "\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>";
-
-                       // Start segment
-                       inWriter.write(trackStart);
-                       // Loop over track points
-                       boolean firstTrackpoint = true;
-                       for (i=0; i<numPoints; i++)
+               }
+               inWriter.write("</Folder>\n</kml>");
+               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<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
+                       + "\t\t\t\t<color>cc" + reverse(ColourUtils.makeHexCode(_colourPatch.getBackground())) + "</color>\n"
+                       + "\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
+                       + "\t\t</Style>\n\t\t<LineString>\n";
+               if (inAbsoluteAltitudes) {
+                       trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
+               }
+               else {
+                       trackStart += "\t\t\t<altitudeMode>clampToGround</altitudeMode>\n";
+               }
+               trackStart += "\t\t\t<coordinates>";
+               String trackEnd = "\t\t\t</coordinates>\n\t\t</LineString>\n\t</Placemark>";
+
+               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<numPoints; i++)
+               {
+                       DataPoint point = _track.getPoint(i);
+                       boolean writeCurrentPoint = !justSelection || (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<Placemark>\n\t\t<name>track</name>\n\t\t<Style>\n\t\t\t<LineStyle>\n"
+                       + "\t\t\t\t<color>cc" + reverse(ColourUtils.makeHexCode(_colourPatch.getBackground())) + "</color>\n"
+                       + "\t\t\t\t<width>4</width>\n\t\t\t</LineStyle>\n"
+                       + "\t\t</Style>\n\t\t<gx:Track>\n";
+               if (inAbsoluteAltitudes) {
+                       trackStart += "\t\t\t<extrude>1</extrude>\n\t\t\t<altitudeMode>absolute</altitudeMode>\n";
+               }
+               else {
+                       trackStart += "\t\t\t<altitudeMode>clampToGround</altitudeMode>\n";
+               }
+               String trackEnd = "\n\t\t</gx:Track>\n\t</Placemark>\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<numPoints; i++)
+               {
+                       DataPoint point = _track.getPoint(i);
+                       boolean writeCurrentPoint = !justSelection || (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("<when>");
+                                       if (point.hasTimestamp()) {
+                                               whenList.append(point.getTimestamp().getText(Timestamp.FORMAT_ISO_8601));
                                        }
-                                       if (point.getPhoto() == null)
-                                       {
-                                               exportTrackpoint(point, inWriter);
-                                               numSaved++;
-                                               firstTrackpoint = false;
+                                       whenList.append("</when>\n");
+                                       // Add coordinates to the list
+                                       coordList.append("<gx:coord>");
+                                       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("</gx:coord>\n");
+                                       numSaved++;
+                                       firstTrackpoint = false;
                                }
                        }
-                       // end segment
-                       inWriter.write(trackEnd);
                }
-               inWriter.write("</Folder>\n</kml>");
+               // 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 (file)
index 0000000..65e23c0
--- /dev/null
@@ -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;
+       }
+}
index ee30841d6d779f7790b95006a58a75eda7759433..ba6605853f560b62eb3df3b3aabdfdd5fe7e8c70 100644 (file)
@@ -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<numlines; i++)
-               {
-                       // write cylinder to file
-                       inWriter.write("object { lat_line translate <0, 0, " + inModel.getScaledLatitudeLine(i) + "> }");
-                       inWriter.write(inLineSeparator);
-               }
-               numlines = inModel.getLongitudeLines().length;
-               for (int i=0; i<numlines; i++)
-               {
-                       // write cylinder to file
-                       inWriter.write("object { lon_line translate <" + inModel.getScaledLongitudeLine(i) + ", 0, 0> }");
-                       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) {
+       }
 }
index 4a86c5b5472c27f0b1b7e201ba658194b793b1f6..33f42f95a75bbbb3d4d671edc44b76a824c481ca 100644 (file)
@@ -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);
        }
 
index 1671607d5e9c1a417b4b75e878aef5aa9465eec4..cfac9afabf1798d87e33f199371ed98ba62fb743 100644 (file)
@@ -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<numlines; i++)
-               {
-                       group.addChild(createLatLine(inModel.getScaledLatitudeLine(i), inModel.getModelSize()));
-               }
-               numlines = inModel.getLongitudeLines().length;
-               for (int i=0; i<numlines; i++)
-               {
-                       group.addChild(createLonLine(inModel.getScaledLongitudeLine(i), inModel.getModelSize()));
-               }
-               return group;
-       }
-
-
-       /**
-        * Make a single latitude line for the specified latitude
-        * @param inLatitude latitude in scaled units
-        * @param inSize size of model, for length of line
-        * @return Group object containing cylinder for latitude line
-        */
-       private static Group createLatLine(double inLatitude, double inSize)
-       {
-               Cylinder latline = new Cylinder(0.1f, (float) (inSize*2));
-               Transform3D horizShift = new Transform3D();
-               horizShift.setTranslation(new Vector3d(0.0, 0.0, -inLatitude));
-               TransformGroup horizTrans = new TransformGroup(horizShift);
-               Transform3D zRot = new Transform3D();
-               zRot.rotZ(Math.toRadians(90.0));
-               TransformGroup zTrans = new TransformGroup(zRot);
-               horizTrans.addChild(zTrans);
-               zTrans.addChild(latline);
-               return horizTrans;
-       }
-
-
-       /**
-        * Make a single longitude line for the specified longitude
-        * @param inLongitude longitude in scaled units
-        * @param inSize size of model, for length of line
-        * @return Group object containing cylinder for longitude line
-        */
-       private static Group createLonLine(double inLongitude, double inSize)
-       {
-               Cylinder lonline = new Cylinder(0.1f, (float) (inSize*2));
-               Transform3D horizShift = new Transform3D();
-               horizShift.setTranslation(new Vector3d(inLongitude, 0.0, 0.0));
-               TransformGroup horizTrans = new TransformGroup(horizShift);
-               Transform3D xRot = new Transform3D();
-               xRot.rotX(Math.toRadians(90.0));
-               TransformGroup xTrans = new TransformGroup(xRot);
-               horizTrans.addChild(xTrans);
-               xTrans.addChild(lonline);
-               return horizTrans;
-       }
-
-
        /**
         * Make a Group of the data points to be added
         * @param inModel model containing data
@@ -442,15 +364,18 @@ public class Java3DWindow implements ThreeDWindow
                                // Add waypoint
                                // Note that x, y and z are horiz, altitude, -vert
                                group.addChild(createWaypoint(new Point3d(
-                                       inModel.getScaledHorizValue(i), inModel.getScaledAltValue(i), -inModel.getScaledVertValue(i))));
+                                       inModel.getScaledHorizValue(i) * MODEL_SCALE_FACTOR,
+                                       inModel.getScaledAltValue(i)   * MODEL_SCALE_FACTOR,
+                                       -inModel.getScaledVertValue(i) * MODEL_SCALE_FACTOR)));
                        }
                        else
                        {
                                // Add colour-coded track point
                                // Note that x, y and z are horiz, altitude, -vert
                                group.addChild(createTrackpoint(new Point3d(
-                                       inModel.getScaledHorizValue(i), inModel.getScaledAltValue(i), -inModel.getScaledVertValue(i)),
-                                       inModel.getPointHeightCode(i)));
+                                       inModel.getScaledHorizValue(i) * MODEL_SCALE_FACTOR,
+                                       inModel.getScaledAltValue(i)   * MODEL_SCALE_FACTOR,
+                                       -inModel.getScaledVertValue(i) * MODEL_SCALE_FACTOR), inModel.getPointHeightCode(i)));
                        }
                }
                return group;
diff --git a/tim/prune/threedee/LineDialog.java b/tim/prune/threedee/LineDialog.java
deleted file mode 100644 (file)
index e371bd0..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-package tim.prune.threedee;
-
-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.JEditorPane;
-import javax.swing.JFrame;
-import javax.swing.JPanel;
-
-import tim.prune.I18nManager;
-import tim.prune.data.Latitude;
-import tim.prune.data.Longitude;
-
-/**
- * Class to show a dialog displaying the line coordinates
- * for a 3d view (either java3d or povray)
- */
-public class LineDialog
-{
-       private JDialog _dialog = null;
-       private JFrame _parent = null;
-       private double[] _latLines = null;
-       private double[] _lonLines = null;
-
-
-       /**
-        * Constructor giving parent frame, latitude and longitude lines
-        * @param inParent parent frame for dialog
-        * @param inLatLines latitude lines as doubles
-        * @param inLonLines longitude lines as doubles
-        */
-       public LineDialog(JFrame inParent, double[] inLatLines, double[] inLonLines)
-       {
-               _parent = inParent;
-               _latLines = inLatLines;
-               _lonLines = inLonLines;
-       }
-
-
-       /**
-        * Show the dialog with the lines
-        */
-       public void showDialog()
-       {
-               _dialog = new JDialog(_parent, I18nManager.getText("dialog.3dlines.title"), true);
-               _dialog.setLocationRelativeTo(_parent);
-               _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
-               _dialog.getContentPane().add(makeDialogComponents());
-               _dialog.pack();
-               _dialog.setVisible(true);
-       }
-
-
-       /**
-        * @return dialog components
-        */
-       private JPanel makeDialogComponents()
-       {
-               JPanel panel = new JPanel();
-               panel.setLayout(new BorderLayout());
-               StringBuffer descBuffer = new StringBuffer();
-               final int numLatLines = (_latLines == null?0:_latLines.length);
-               final int numLonLines = (_lonLines == null?0:_lonLines.length);
-               if (numLatLines == 0 && numLonLines == 0)
-               {
-                       descBuffer.append("<p>").append(I18nManager.getText("dialog.3dlines.empty")).append("</p>");
-               }
-               else
-               {
-                       descBuffer.append("<p>").append(I18nManager.getText("dialog.3dlines.intro")).append(":</p>");
-                       descBuffer.append("<p>").append(I18nManager.getText("fieldname.latitude")).append("<ul>");
-                       Latitude lat = null;
-                       for (int i=0; i<numLatLines; i++)
-                       {
-                               lat = new Latitude(_latLines[i], Latitude.FORMAT_DEG);
-                               descBuffer.append("<li>").append(lat.output(Latitude.FORMAT_DEG_WHOLE_MIN)).append("</li>");
-                       }
-                       descBuffer.append("</ul></p>");
-                       descBuffer.append("<p>").append(I18nManager.getText("fieldname.longitude")).append("<ul>");
-                       Longitude lon = null;
-                       for (int i=0; i<numLonLines; i++)
-                       {
-                               lon = new Longitude(_lonLines[i], Longitude.FORMAT_DEG);
-                               descBuffer.append("<li>").append(lon.output(Longitude.FORMAT_DEG_WHOLE_MIN)).append("</li>");
-                       }
-                       descBuffer.append("</ul></p>");
-               }
-               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;
-       }
-}
index 2c9ac97784e79f24e2c378766283720e354c8a15..ddf91623b717309daf1bb3bfcd82dadefe850097 100644 (file)
@@ -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;
-       }
 }
index ba2b31ab5ef89540b8496be4e8908f0a9855ae7e..58f09e9c267afd867c104fa316fc058878367b89 100644 (file)
@@ -14,7 +14,7 @@ public class UndoDeleteRange implements UndoOperation
        /**\r
         * Inner class to hold a single range information set\r
         */\r
-       class RangeInfo\r
+       static class RangeInfo\r
        {\r
                public int _startIndex = -1;\r
                public DataPoint[] _points = null;\r