]> gitweb.fperrin.net Git - GpsPrune.git/commitdiff
Merge remote-tracking branch 'upstream/master' into fp-integration
authorFrédéric Perrin <fred@fperrin.net>
Tue, 20 Oct 2020 19:25:42 +0000 (20:25 +0100)
committerFrédéric Perrin <fred@fperrin.net>
Tue, 20 Oct 2020 19:25:42 +0000 (20:25 +0100)
14 files changed:
1  2 
buildtools/build.sh
pom.xml
src/tim/prune/App.java
src/tim/prune/FunctionLibrary.java
src/tim/prune/GpsPrune.java
src/tim/prune/config/Config.java
src/tim/prune/data/FileInfo.java
src/tim/prune/function/AddTimeOffset.java
src/tim/prune/function/srtm/LookupSrtmFunction.java
src/tim/prune/gui/MenuManager.java
src/tim/prune/gui/Viewport.java
src/tim/prune/gui/map/MapCanvas.java
src/tim/prune/gui/map/MapTileManager.java
src/tim/prune/lang/prune-texts_en.properties

diff --combined buildtools/build.sh
index d2311b590dc5f17e18c85aedd412314a0ac0e4e4,193244bb3319fa81e974dd1d9f09fbbb52c7f70c..ee29ad85452faf1e7f530a91eeb93b791313a5a2
mode 100755,100644..100755
@@@ -1,7 -1,7 +1,8 @@@
 +set -e
  # Build script
+ set -e
  # Version number
- PRUNENAME=gpsprune_19.2
+ PRUNENAME=gpsprune_20
  # remove compile directory
  rm -rf compile
  # remove dist directory
@@@ -17,8 -17,6 +18,8 @@@ cp -r src/tim/prune/lang compile/tim/pr
  cp -r src/tim/prune/*.txt compile/tim/prune/
  cp -r src/tim/prune/gui/images compile/tim/prune/gui/
  cp src/tim/prune/function/srtm/srtmtiles.dat compile/tim/prune/function/srtm
 +mkdir compile/tim/prune/function/srtm/viewfinder/
 +cp src/tim/prune/function/srtm/viewfinder/tiles.dat compile/tim/prune/function/srtm/viewfinder/
  # make dist directory
  mkdir dist
  # build into jar file
diff --combined pom.xml
index f3a608c7f0e6717aa12f921a793b179a0d02eb8c,15b6562639083b2eba02a431e820e46a75c8e0e9..2cb419374a729bd82b2a7965297ab7420b474a48
+++ b/pom.xml
@@@ -7,7 -7,7 +7,7 @@@
  
        <groupId>tim.prune</groupId>
        <artifactId>gpsprune</artifactId>
-       <version>19.2</version>
+       <version>20</version>
        <packaging>jar</packaging>
  
        <name>tim.prune.gpsprune</name>
@@@ -57,7 -57,6 +57,7 @@@
                                        <include>tim/prune/gui/images/*</include>
                                        <include>tim/prune/lang/*</include>
                                        <include>tim/prune/function/srtm/srtmtiles.dat</include>
 +                                      <include>tim/prune/function/srtm/viewfinder/tiles.dat</include>
                                        <include>tim/prune/*.txt</include>
                                </includes>
                        </resource>
                                <plugin>
                                        <artifactId>maven-compiler-plugin</artifactId>
                                        <version>3.8.0</version>
 +                                              <configuration>
 +          <compilerArgs>
 +            <arg>-Xlint:deprecation</arg>
 +          </compilerArgs>
 +        </configuration>
                                </plugin>
                                <plugin>
                                        <artifactId>maven-jar-plugin</artifactId>
diff --combined src/tim/prune/App.java
index 317064d39984acb566cee90940236c52c32fd58f,94e10e0e2ed873e6bbf7558419de14e393c20f08..f76edb177d2ffb4f8ba09e3feda928022be052d5
@@@ -51,7 -51,6 +51,7 @@@ public class Ap
  {
        // Instance variables
        private JFrame _frame = null;
 +      private String _titlePrefix = null;
        private Track _track = null;
        private TrackInfo _trackInfo = null;
        private int _lastSavePosition = 0;
@@@ -70,7 -69,7 +70,7 @@@
        private AppMode _appMode = AppMode.NORMAL;
  
        /** Enum for the app mode - currently only two options but may expand later */
-       public enum AppMode {NORMAL, DRAWRECT};
+       public enum AppMode {NORMAL, DRAWRECT}
  
  
        /**
@@@ -80,7 -79,6 +80,7 @@@
        public App(JFrame inFrame)
        {
                _frame = inFrame;
 +              _titlePrefix = _frame.getTitle();
                _undoStack = new UndoStack();
                _track = new Track();
                _trackInfo = new TrackInfo(_track);
        }
  
  
 +      /**
 +       * Remove altitudes from selected points
 +       */
 +      public void removeAltitudes(int selStart, int selEnd)
 +      {
 +              UndoRemoveAltitudes undo = new UndoRemoveAltitudes(_trackInfo, selStart, selEnd);
 +              if (_trackInfo.getTrack().removeAltitudes(selStart, selEnd))
 +              {
 +                      _undoStack.add(undo);
 +                      _trackInfo.getSelection().markInvalid();
 +                      UpdateMessageBroker.informSubscribers(DataSubscriber.DATA_EDITED);
 +                      UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.removealtitudes"));
 +              }
 +      }
 +
 +
        /**
         * Merge the track segments within the current selection
         */
                loadedTrack.load(inFieldArray, inDataArray, inOptions);
                if (loadedTrack.getNumPoints() <= 0)
                {
-                       showErrorMessage("error.load.dialogtitle", "error.load.nopoints");
+                       String msgKey = (inSourceInfo == null ? "error.load.nopointsintext" : "error.load.nopoints");
+                       showErrorMessage("error.load.dialogtitle", msgKey);
                        // load next file if there's a queue
                        loadNextFile();
                        return;
                                undo.setNumPhotosAudios(_trackInfo.getPhotoList().getNumPhotos(), _trackInfo.getAudioList().getNumAudios());
                                _undoStack.add(undo);
                                _track.combine(inLoadedTrack);
-                               // set source information
-                               inSourceInfo.populatePointObjects(_track, inLoadedTrack.getNumPoints());
-                               _trackInfo.getFileInfo().addSource(inSourceInfo);
+                               if (inSourceInfo != null)
+                               {
+                                       // set source information
+                                       inSourceInfo.populatePointObjects(_track, inLoadedTrack.getNumPoints());
+                                       _trackInfo.getFileInfo().addSource(inSourceInfo);
+                               }
                        }
                        else if (answer == JOptionPane.NO_OPTION)
                        {
                                _lastSavePosition = _undoStack.size();
                                _trackInfo.getSelection().clearAll();
                                _track.load(inLoadedTrack);
-                               inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
-                               _trackInfo.getFileInfo().replaceSource(inSourceInfo);
+                               if (inSourceInfo != null)
+                               {
+                                       // set source information
+                                       inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
+                                       _trackInfo.getFileInfo().replaceSource(inSourceInfo);
+                               }
                                _trackInfo.getPhotoList().removeCorrelatedPhotos();
                                _trackInfo.getAudioList().removeCorrelatedAudios();
                        }
                        _lastSavePosition = _undoStack.size();
                        _trackInfo.getSelection().clearAll();
                        _track.load(inLoadedTrack);
-                       inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
-                       _trackInfo.getFileInfo().addSource(inSourceInfo);
+                       if (inSourceInfo != null)
+                       {
+                               inSourceInfo.populatePointObjects(_track, _track.getNumPoints());
+                               _trackInfo.getFileInfo().addSource(inSourceInfo);
+                       }
                }
                // Update config before subscribers are told
-               boolean isRegularLoad = (inSourceInfo.getFileType() != FILE_TYPE.GPSBABEL);
-               Config.getRecentFileList().addFile(new RecentFile(inSourceInfo.getFile(), isRegularLoad));
+               if (inSourceInfo != null)
+               {
+                       boolean isRegularLoad = (inSourceInfo.getFileType() != FILE_TYPE.GPSBABEL);
+                       Config.getRecentFileList().addFile(new RecentFile(inSourceInfo.getFile(), isRegularLoad));
+                       // Update status bar
+                       UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile")
+                               + " '" + inSourceInfo.getName() + "'");
+               }
                UpdateMessageBroker.informSubscribers();
-               // Update status bar
-               UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.loadfile")
-                       + " '" + inSourceInfo.getName() + "'");
                // update menu
                _menuManager.informFileLoaded();
 +              // recentre viewport on new file data
 +              _viewport.recentreViewport();
 +              // update main window title
 +              updateTitle();
                // Remove busy lock
                _busyLoading = false;
                // load next file if there's a queue
        public void setCurrentMode(AppMode inMode) {
                _appMode = inMode;
        }
 +
 +      /** Update main window title **/
 +      public void updateTitle() {
 +              ArrayList<String> filenames = _trackInfo.getFileInfo().getFilenames();
 +              if (filenames.size() > 0) {
 +                      _frame.setTitle(_titlePrefix + ": " + String.join(", ", filenames));
 +              }
 +              else
 +              {
 +                      _frame.setTitle(_titlePrefix);
 +              }
 +      }
  }
index 4be286e479d036a8b37cabe8223f495872a2f361,68303ed95946466daf23cf1adc5b15aa24f31786..58d049d34d18b24e69b510b944df34f301b16c56
@@@ -18,17 -18,16 +18,17 @@@ import tim.prune.function.DiskCacheConf
  import tim.prune.function.DownloadOsmFunction;
  import tim.prune.function.DuplicatePoint;
  import tim.prune.function.FindWaypoint;
- import tim.prune.function.FullRangeDetails;
+ import tim.prune.function.ProjectPoint;
+ import tim.prune.function.ShowFullDetails;
  import tim.prune.function.GetWikipediaFunction;
  import tim.prune.function.HelpScreen;
  import tim.prune.function.IgnoreExifThumb;
  import tim.prune.function.InterpolateFunction;
- import tim.prune.function.PasteCoordinates;
  import tim.prune.function.PhotoPopupFunction;
  import tim.prune.function.PlayAudioFunction;
  import tim.prune.function.RearrangePhotosFunction;
  import tim.prune.function.RearrangeWaypointsFunction;
 +import tim.prune.function.RemoveAltitudes;
  import tim.prune.function.RemoveAudioFunction;
  import tim.prune.function.RemovePhotoFunction;
  import tim.prune.function.RotatePhoto;
@@@ -51,18 -50,16 +51,16 @@@ import tim.prune.function.distance.Dist
  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.settings.SaveConfig;
  import tim.prune.function.settings.SetAltitudeTolerance;
  import tim.prune.function.settings.SetColours;
  import tim.prune.function.settings.SetDisplaySettings;
 +import tim.prune.function.settings.SetEarthdataAuthentication;
  import tim.prune.function.settings.SetLanguage;
  import tim.prune.function.settings.SetMapBgFunction;
  import tim.prune.function.settings.SetPathsFunction;
  import tim.prune.function.sew.SewTrackSegmentsFunction;
  import tim.prune.function.sew.SplitSegmentsFunction;
 -import tim.prune.function.srtm.DownloadSrtmFunction;
  import tim.prune.function.srtm.LookupSrtmFunction;
  import tim.prune.function.weather.GetWeatherForecastFunction;
  import tim.prune.load.AudioLoader;
@@@ -89,6 -86,7 +87,7 @@@ public abstract class FunctionLibrar
        public static GenericFunction FUNCTION_IMPORTBABEL = null;
        public static GenericFunction FUNCTION_SAVECONFIG  = null;
        public static GenericFunction FUNCTION_EDIT_WAYPOINT_NAME = null;
+       public static GenericFunction FUNCTION_PROJECT_POINT = null;
        public static GenericFunction FUNCTION_REARRANGE_WAYPOINTS = null;
        public static GenericFunction FUNCTION_SELECT_SEGMENT = null;
        public static GenericFunction FUNCTION_SPLIT_SEGMENTS = null;
        public static GenericFunction FUNCTION_DELETE_BY_DATE = null;
        public static SingleNumericParameterFunction FUNCTION_INTERPOLATE = null;
        public static GenericFunction FUNCTION_LOOKUP_SRTM = null;
 -      public static GenericFunction FUNCTION_DOWNLOAD_SRTM = null;
        public static GenericFunction FUNCTION_NEARBY_WIKIPEDIA = null;
        public static GenericFunction FUNCTION_SEARCH_WIKIPEDIA = null;
        public static GenericFunction FUNCTION_SEARCH_OSMPOIS = null;
        public static GenericFunction FUNCTION_DOWNLOAD_OSM = null;
        public static GenericFunction FUNCTION_ADD_TIME_OFFSET  = null;
        public static GenericFunction FUNCTION_ADD_ALTITUDE_OFFSET  = null;
 +      public static GenericFunction FUNCTION_REMOVE_ALTITUDES = null;
        public static GenericFunction FUNCTION_CONVERT_NAMES_TO_TIMES  = null;
        public static GenericFunction FUNCTION_DELETE_FIELD_VALUES  = null;
-       public static GenericFunction FUNCTION_PASTE_COORDINATES = null;
        public static GenericFunction FUNCTION_FIND_WAYPOINT = null;
        public static GenericFunction FUNCTION_DUPLICATE_POINT = null;
        public static GenericFunction FUNCTION_CONNECT_TO_POINT = null;
        public static GenericFunction FUNCTION_CHARTS = null;
        public static GenericFunction FUNCTION_3D     = null;
        public static GenericFunction FUNCTION_DISTANCES  = null;
-       public static GenericFunction FUNCTION_FULL_RANGE_DETAILS = null;
+       public static GenericFunction FUNCTION_FULL_DETAILS = null;
        public static GenericFunction FUNCTION_AUTOPLAY_TRACK = 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_GET_WEATHER_FORECAST = null;
        public static GenericFunction FUNCTION_LOAD_AUDIO = null;
        public static GenericFunction FUNCTION_REMOVE_AUDIO = null;
        public static GenericFunction FUNCTION_SET_COLOURS = null;
        public static GenericFunction FUNCTION_SET_LANGUAGE = null;
        public static SingleNumericParameterFunction FUNCTION_SET_ALTITUDE_TOLERANCE = null;
 +      public static GenericFunction FUNCTION_SET_EARTHDATA_AUTH = null;
        public static GenericFunction FUNCTION_SET_TIMEZONE = null;
        public static GenericFunction FUNCTION_HELP   = null;
        public static GenericFunction FUNCTION_SHOW_KEYS = null;
                FUNCTION_IMPORTBABEL = new BabelLoadFromFile(inApp);
                FUNCTION_SAVECONFIG = new SaveConfig(inApp);
                FUNCTION_EDIT_WAYPOINT_NAME = new PointNameEditor(inApp);
+               FUNCTION_PROJECT_POINT = new ProjectPoint(inApp);
                FUNCTION_REARRANGE_WAYPOINTS = new RearrangeWaypointsFunction(inApp);
                FUNCTION_SELECT_SEGMENT = new SelectSegmentFunction(inApp);
                FUNCTION_SPLIT_SEGMENTS = new SplitSegmentsFunction(inApp);
                FUNCTION_DELETE_BY_DATE = new DeleteByDateFunction(inApp);
                FUNCTION_INTERPOLATE = new InterpolateFunction(inApp);
                FUNCTION_LOOKUP_SRTM = new LookupSrtmFunction(inApp);
 -              FUNCTION_DOWNLOAD_SRTM = new DownloadSrtmFunction(inApp);
                FUNCTION_NEARBY_WIKIPEDIA = new GetWikipediaFunction(inApp);
                FUNCTION_SEARCH_WIKIPEDIA = new SearchWikipediaNames(inApp);
                FUNCTION_SEARCH_OSMPOIS = new SearchOsmPoisFunction(inApp);
                FUNCTION_DOWNLOAD_OSM = new DownloadOsmFunction(inApp);
                FUNCTION_ADD_TIME_OFFSET = new AddTimeOffset(inApp);
                FUNCTION_ADD_ALTITUDE_OFFSET = new AddAltitudeOffset(inApp);
 +              FUNCTION_REMOVE_ALTITUDES = new RemoveAltitudes(inApp);
                FUNCTION_CONVERT_NAMES_TO_TIMES = new ConvertNamesToTimes(inApp);
                FUNCTION_DELETE_FIELD_VALUES = new DeleteFieldValues(inApp);
-               FUNCTION_PASTE_COORDINATES = new PasteCoordinates(inApp);
                FUNCTION_FIND_WAYPOINT = new FindWaypoint(inApp);
                FUNCTION_DUPLICATE_POINT = new DuplicatePoint(inApp);
                FUNCTION_CONNECT_TO_POINT = new ConnectToPointFunction(inApp);
                FUNCTION_CHARTS = new Charter(inApp);
                FUNCTION_3D     = new ShowThreeDFunction(inApp);
                FUNCTION_DISTANCES = new DistanceFunction(inApp);
-               FUNCTION_FULL_RANGE_DETAILS = new FullRangeDetails(inApp);
+               FUNCTION_FULL_DETAILS = new ShowFullDetails(inApp);
                FUNCTION_AUTOPLAY_TRACK = new AutoplayFunction(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_GET_WEATHER_FORECAST = new GetWeatherForecastFunction(inApp);
                FUNCTION_LOAD_AUDIO = new AudioLoader(inApp);
                FUNCTION_REMOVE_AUDIO = new RemoveAudioFunction(inApp);
                FUNCTION_SET_LANGUAGE = new SetLanguage(inApp);
                FUNCTION_SET_ALTITUDE_TOLERANCE = new SetAltitudeTolerance(inApp);
                FUNCTION_SET_TIMEZONE = new SelectTimezoneFunction(inApp);
 +              FUNCTION_SET_EARTHDATA_AUTH = new SetEarthdataAuthentication(inApp);
                FUNCTION_HELP   = new HelpScreen(inApp);
                FUNCTION_SHOW_KEYS = new ShowKeysScreen(inApp);
                FUNCTION_ABOUT  = new AboutScreen(inApp);
index a9b53f402130a024dda7610d6a30e118d8287fe8,874ece5a7b7dce6314c5179525b516cf88e31e56..f2bb7bd67e98d601faa7944c83d762fc564dc95d
@@@ -9,9 -9,11 +9,11 @@@ import java.io.File
  import java.io.FileNotFoundException;
  import java.util.ArrayList;
  import java.util.Locale;
  import javax.swing.JFrame;
  import javax.swing.JSplitPane;
  import javax.swing.JToolBar;
+ import javax.swing.UIManager;
  import javax.swing.WindowConstants;
  
  import tim.prune.config.Config;
@@@ -29,16 -31,16 +31,16 @@@ import tim.prune.gui.profile.ProfileCha
  /**
   * GpsPrune is a tool to visualize, edit, convert and prune GPS data
   * Please see the included readme.txt or https://activityworkshop.net
-  * This software is copyright activityworkshop.net 2006-2018 and made available through the Gnu GPL version 2.
+  * This software is copyright activityworkshop.net 2006-2020 and made available through the Gnu GPL version 2.
   * For license details please see the included license.txt.
   * GpsPrune is the main entry point to the application, including initialisation and launch
   */
  public class GpsPrune
  {
        /** Version number of application, used in about screen and for version check */
-       public static final String VERSION_NUMBER = "19.2";
+       public static final String VERSION_NUMBER = "20";
        /** Build number, just used for about screen */
-       public static final String BUILD_NUMBER = "363d";
+       public static final String BUILD_NUMBER = "378";
        /** Static reference to App object */
        private static App APP = null;
  
                                Config.setConfigString(Config.KEY_LANGUAGE_FILE, "");
                        }
                }
+               // Set look-and-feel
+               try {
+                       String windowStyle = Config.getConfigString(Config.KEY_WINDOW_STYLE);
+                       UIManager.setLookAndFeel(windowStyle);
+               }
+               catch (Exception e) {}
                // Set up the window and go
                launch(dataFiles);
        }
                frame.setVisible(true);
                // Set position of map/profile splitter
                midSplit.setDividerLocation(0.75);
 -              // Update menu (only needed for recent file list)
 -              UpdateMessageBroker.informSubscribers();
  
                // Make a full screen toggler
                SidebarController fsc = new SidebarController(new Component[] {leftPanel, profileDisp, rightPanel},
index 3939c004ae6a68d92f9f667bb0533b84abadf0c0,8c6eefec498706d0c789ad1ae3b4302a8e4022ac..35923d73c0123ca3d9180bc586d3cef3d3b9fbbb
@@@ -55,6 -55,8 +55,8 @@@ public abstract class Confi
        public static final String KEY_POVRAY_FONT = "prune.povrayfont";
        /** Key for the selected unit set */
        public static final String KEY_UNITSET_KEY  = "prune.unitsetkey";
+       /** Key for the selected coordinate display format */
+       public static final String KEY_COORD_DISPLAY_FORMAT  = "prune.coorddisplay";
        /** Key for index of map source */
        public static final String KEY_MAPSOURCE_INDEX = "prune.mapsource";
        /** Key for number of fixed map sources */
@@@ -87,6 -89,8 +89,8 @@@
        public static final String KEY_ANTIALIAS = "prune.antialias";
        /** Key for kml track colour */
        public static final String KEY_KML_TRACK_COLOUR = "prune.kmltrackcolour";
+       /** Key for window style (name of look-and-feel) */
+       public static final String KEY_WINDOW_STYLE = "prune.windowstyle";
        /** Key for autosaving settings */
        public static final String KEY_AUTOSAVE_SETTINGS = "prune.autosavesettings";
        /** Key for recently used files */
        public static final String KEY_WAYPOINT_ICON_SIZE = "prune.waypointiconsize";
        /** Id of selected timezone */
        public static final String KEY_TIMEZONE_ID = "prune.timezoneid";
 +      /** Username/password to the Earthdata server for SRTM 1-arcsecond tiles */
 +      public static final String KEY_EARTHDATA_AUTH = "prune.earthdataauth";
  
  
        /** Initialise the default properties */
                _unitSet = UnitSetLibrary.getUnitSet(_configValues.getProperty(KEY_UNITSET_KEY));
                // Adjust map source index if necessary
                adjustSelectedMap();
+               // Reset coord display format
+               setConfigInt(KEY_COORD_DISPLAY_FORMAT, 0);
  
                if (loadFailed) {
                        throw new ConfigException();
                props.put(KEY_ANTIALIAS, "1"); // antialias on by default
                props.put(KEY_AUTOSAVE_SETTINGS, "0"); // autosave false by default
                props.put(KEY_UNITSET_KEY, "unitset.kilometres"); // metric by default
+               props.put(KEY_COORD_DISPLAY_FORMAT, "0"); // original
                props.put(KEY_HEIGHT_EXAGGERATION, "100"); // 100%, no exaggeration
                props.put(KEY_TERRAIN_GRID_SIZE, "50");
                props.put(KEY_ALTITUDE_TOLERANCE, "0"); // 0, all exact as before
                return _configFile;
        }
  
+       /**
+        * Set the file to which config was saved
+        */
+       public static void setConfigFile(File inFile)
+       {
+               _configFile = inFile;
+       }
        /**
         * @return config Properties object to allow all config values to be saved
         */
        public static void updatePointColourer(PointColourer inColourer)
        {
                _pointColourer = inColourer;
-               setConfigString(KEY_POINT_COLOURER, ColourerFactory.PointColourerToString(_pointColourer));
+               setConfigString(KEY_POINT_COLOURER, ColourerFactory.pointColourerToString(_pointColourer));
        }
  
        /**
index 7eaac8f307d1bcde51f68a3bf662cd6879adeab2,0c4ee5fd4606624b8de9ce00af8f57a3284ccee1..6ca812ef6db864e421505e478ba3b31c084bbd01
@@@ -51,7 -51,9 +51,9 @@@ public class FileInf
         */
        public void removeSource()
        {
-               _sources.remove(_sources.size()-1);
+               if (!_sources.isEmpty()) {
+                       _sources.remove(_sources.size()-1);
+               }
        }
  
        /**
                return "";
        }
  
 +      /**
 +       * @return The source names
 +       */
 +      public ArrayList<String> getFilenames()
 +      {
 +              ArrayList<String> filenames = new ArrayList<String>();
 +              for (SourceInfo source : _sources)
 +              {
 +                      filenames.add(source.getName());
 +              }
 +              return filenames;
 +      }
 +
        /**
         * @param inIndex index number, starting from zero
         * @return source info object
index 199ae5056b133aa27f59b3f509cf9806f9e4dca2,115bfa5997102edc0db005074c05fa5a97bb4754..ed75a5ec7104d441770435ee56dbc290000cfb86
@@@ -33,8 -33,8 +33,8 @@@ public class AddTimeOffset extends Gene
  {
        private JDialog _dialog = null;
        private JRadioButton _addRadio = null, _subtractRadio = null;
 -      private WholeNumberField _dayField = null, _hourField = null;
 -      private WholeNumberField _minuteField = null;
 +      private WholeNumberField _1024weekField = null, _dayField = null;
 +      private WholeNumberField _hourField = null, _minuteField = null;
        private JButton _okButton = null;
  
  
                // Make a central panel with the text boxes
                JPanel descPanel = new JPanel();
                descPanel.setLayout(new GridLayout(0, 2));
 +              descPanel.add(makeRightLabel("dialog.addtimeoffset.1024week"));
 +              _1024weekField = new WholeNumberField(3);
 +              descPanel.add(_1024weekField);
                descPanel.add(makeRightLabel("dialog.addtimeoffset.days"));
 -              _dayField = new WholeNumberField(3);
 +              _dayField = new WholeNumberField(4);
                descPanel.add(_dayField);
                descPanel.add(makeRightLabel("dialog.addtimeoffset.hours"));
                _hourField = new WholeNumberField(3);
                MouseAdapter mouseListener = new MouseAdapter() {
                        public void mouseReleased(java.awt.event.MouseEvent arg0) {
                                _okButton.setEnabled(getOffsetSecs() != 0L);
-                       };
+                       }
                };
 +              _1024weekField.addKeyListener(keyListener);
                _dayField.addKeyListener(keyListener);
                _hourField.addKeyListener(keyListener);
                _minuteField.addKeyListener(keyListener);
 +              _1024weekField.addMouseListener(mouseListener);
                _dayField.addMouseListener(mouseListener);
                _hourField.addMouseListener(mouseListener);
                _minuteField.addMouseListener(mouseListener);
        {
                long offsetSecs = _minuteField.getValue() * 60L
                  + _hourField.getValue() * 60L * 60L
 -                + _dayField.getValue() * 60L * 60L * 24L;
 +                + _dayField.getValue() * 60L * 60L * 24L
 +                + _1024weekField.getValue() * 60L * 60L * 24L * 7L * 1024L;
                if (_subtractRadio.isSelected()) {offsetSecs = -offsetSecs;}
                return offsetSecs;
        }
index 947cca9294b53c54a529684255cd063fcdd3c296,c48af016b889f0df84a094de2df84db0db0ef272..8f4dc2e01767fb18237dd39380e6943bfb29567d
@@@ -1,6 -1,12 +1,6 @@@
  package tim.prune.function.srtm;
  
 -import java.io.File;
 -import java.io.FileInputStream;
 -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.JOptionPane;
  
@@@ -9,12 -15,14 +9,12 @@@ import tim.prune.DataSubscriber
  import tim.prune.GenericFunction;
  import tim.prune.I18nManager;
  import tim.prune.UpdateMessageBroker;
 -import tim.prune.config.Config;
  import tim.prune.data.Altitude;
  import tim.prune.data.DataPoint;
  import tim.prune.data.Field;
  import tim.prune.data.Track;
  import tim.prune.data.UnitSetLibrary;
  import tim.prune.gui.ProgressDialog;
 -import tim.prune.tips.TipManager;
  import tim.prune.undo.UndoLookupSrtm;
  
  /**
@@@ -30,9 -38,13 +30,9 @@@ public class LookupSrtmFunction extend
        private Track _track = null;
        /** Flag for whether this is a real track or a terrain one */
        private boolean _normalTrack = true;
 -      /** Flag set when any tiles had to be downloaded (rather than just loaded locally) */
 -      private boolean _hadToDownload = false;
        /** Flag to check whether this function is currently running or not */
        private boolean _running = false;
  
 -      /** Expected size of hgt file in bytes */
 -      private static final long HGT_SIZE = 2884802L;
        /** Altitude below which is considered void */
        private static final int VOID_VAL = -32768;
  
        private void begin(Track inTrack, boolean inNormalTrack)
        {
                _running = true;
 -              _hadToDownload = false;
 +              if (! SrtmDiskCache.ensureCacheIsUsable())
 +              {
 +                      _app.showErrorMessage(getNameKey(), "error.cache.notthere");
 +              }
                if (_progress == null) {
                        _progress = new ProgressDialog(_parentFrame, getNameKey());
                }
                for (int i = 0; i < _track.getNumPoints(); i++)
                {
                        // Consider points which don't have altitudes or have zero values
 -                      if (!_track.getPoint(i).hasAltitude()
 -                              || (overwriteZeros && _track.getPoint(i).getAltitude().getValue() == 0))
 +                      if (needsAltitude(_track.getPoint(i), overwriteZeros))
                        {
                                SrtmTile tile = new SrtmTile(_track.getPoint(i));
                                boolean alreadyGot = false;
                lookupValues(tileList, overwriteZeros);
                // Finished
                _running = false;
 -              // Show tip if lots of online lookups were necessary
 -              if (_hadToDownload) {
 -                      _app.showTip(TipManager.Tip_DownloadSrtm);
 -              }
        }
  
 +      /**
 +       * true if we need to set the altitude of this point
 +       */
 +      private boolean needsAltitude(DataPoint point, boolean overwriteZeros)
 +      {
 +              if (!point.hasAltitude())
 +              {
 +                      return true;
 +              }
 +              if (overwriteZeros && point.getAltitude().getValue() == 0)
 +              {
 +                      return true;
 +              }
 +              return false;
 +      }
  
        /**
         * Lookup the values from SRTM data
                        _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() && !_progress.isCancelled() && urls != null; t++)
 +              String errorMessage = "";
 +              for (int t=0; t<inTileList.size() && !_progress.isCancelled(); t++)
                {
 -                      if (urls[t] != null)
 +                      SrtmTile tile = inTileList.get(t);
 +                      SrtmSource srtmSource = tile.findBestCachedSource();
 +
 +                      if (srtmSource == null)
                        {
 -                              SrtmTile tile = inTileList.get(t);
 -                              try
 -                              {
 -                                      // Set progress
 -                                      _progress.setValue(t);
 -                                      final int ARRLENGTH = 1201 * 1201;
 -                                      int[] heights = new int[ARRLENGTH];
 -                                      // Open zipinputstream on url and check size
 -                                      ZipInputStream inStream = getStreamToHgtFile(urls[t]);
 -                                      boolean entryOk = false;
 -                                      if (inStream != null)
 -                                      {
 -                                              ZipEntry entry = inStream.getNextEntry();
 -                                              entryOk = (entry != null && entry.getSize() == HGT_SIZE);
 -                                              if (entryOk)
 -                                              {
 -                                                      // Read entire file contents into one byte array
 -                                                      for (int i = 0; i < ARRLENGTH; i++)
 -                                                      {
 -                                                              heights[i] = inStream.read() * 256 + inStream.read();
 -                                                              if (heights[i] >= 32768) {heights[i] -= 65536;}
 -                                                      }
 -                                              }
 -                                              // else {
 -                                              //      System.out.println("length not ok: " + entry.getSize());
 -                                              // }
 -                                              // Close stream from url
 -                                              inStream.close();
 -                                      }
 +                              errorMessage += "Tile "+tile.getTileName()+" not in cache!\n";
 +                              continue;
 +                      }
  
 -                                      if (entryOk)
 -                                      {
 -                                              numAltitudesFound += applySrtmTileToWholeTrack(tile, heights, inOverwriteZeros);
 -                                      }
 -                              }
 -                              catch (IOException ioe) {
 -                                      errorMessage = ioe.getClass().getName() + " - " + ioe.getMessage();
 -                              }
 +                      // Set progress
 +                      _progress.setValue(t);
 +
 +                      int[] heights;
 +                      try {
 +                              heights = srtmSource.getTileHeights(tile);
 +                      }
 +                      catch (SrtmSourceException e)
 +                      {
 +                              errorMessage += e.getMessage();
 +                              e.printStackTrace();
 +                              continue;
                        }
-                       // Loop over all points in track, try to apply altitude from array
-                       for (int p = 0; p < _track.getNumPoints(); p++)
-                       {
-                               DataPoint point = _track.getPoint(p);
-                               if (needsAltitude(point, inOverwriteZeros))
-                               {
-                                       if (new SrtmTile(point).equals(tile))
-                                       {
-                                               double x = (point.getLongitude().getDouble() - tile.getLongitude()) * (rowSize - 1);
-                                               double y = rowSize - (point.getLatitude().getDouble() - tile.getLatitude()) * (rowSize - 1);
-                                               int idx1 = ((int)y)*rowSize + (int)x;
-                                               try
-                                               {
-                                                       int[] fouralts = {heights[idx1], heights[idx1+1], heights[idx1-rowSize], heights[idx1-rowSize+1]};
-                                                       int numVoids = (fouralts[0]==VOID_VAL?1:0) + (fouralts[1]==VOID_VAL?1:0)
-                                                               + (fouralts[2]==VOID_VAL?1:0) + (fouralts[3]==VOID_VAL?1:0);
-                                                       // if (numVoids > 0) System.out.println(numVoids + " voids found");
-                                                       double altitude = 0.0;
-                                                       switch (numVoids)
-                                                       {
-                                                       case 0: altitude = bilinearInterpolate(fouralts, x, y); break;
-                                                       case 1: altitude = bilinearInterpolate(fixVoid(fouralts), x, y); break;
-                                                       case 2:
-                                                       case 3: altitude = averageNonVoid(fouralts); break;
-                                                       default: altitude = VOID_VAL;
-                                                       }
-                                                       // Special case for terrain tracks, don't interpolate voids yet
-                                                       if (!_normalTrack && numVoids > 0) {
-                                                               altitude = VOID_VAL;
-                                                       }
-                                                       if (altitude != VOID_VAL)
-                                                       {
-                                                               point.setFieldValue(Field.ALTITUDE, ""+altitude, false);
-                                                               // depending on settings, this value may have been added as feet, we need to force metres
-                                                               point.getAltitude().reset(new Altitude((int)altitude, UnitSetLibrary.UNITS_METRES));
-                                                               numAltitudesFound++;
-                                                       }
-                                               }
-                                               catch (ArrayIndexOutOfBoundsException obe) {
-                                                       errorMessage += "Point not in tile? lat=" + point.getLatitude().getDouble() + ", x=" + x + ", y=" + y + ", idx=" + idx1+"\n";
-                                               }
-                                       }
-                               }
-                       }
 +                      int rowSize = srtmSource.getRowSize(tile);
 +                      if (rowSize <= 0)
 +                      {
 +                              errorMessage += "Tile "+tile.getTileName()+" is corrupted";
 +                      }
 +
++                      numAltitudesFound += applySrtmTimeToWholeTrack(tile, heights, rowSize, inOverwriteZeros);
                }
  
                _progress.dispose();
                        return;
                }
  
 +              if (! errorMessage.equals("")) {
 +                      _app.showErrorMessageNoLookup(getNameKey(), errorMessage);
 +                      return;
 +              }
                if (numAltitudesFound > 0)
                {
                        // Inform app including undo information
                                        I18nManager.getTextWithNumber("confirm.lookupsrtm", numAltitudesFound));
                        }
                }
 -              else if (errorMessage != null) {
 -                      _app.showErrorMessageNoLookup(getNameKey(), errorMessage);
 -              }
                else if (inTileList.size() > 0) {
                        _app.showErrorMessage(getNameKey(), "error.lookupsrtm.nonefound");
                }
                }
        }
  
 -      /**
 -       * See whether the SRTM file is already available locally first, then try online
 -       * @param inUrl URL for online resource
 -       * @return ZipInputStream either on the local file or on the downloaded zip file
 -       */
 -      private ZipInputStream getStreamToHgtFile(URL inUrl)
 -      throws IOException
 -      {
 -              String diskCachePath = Config.getConfigString(Config.KEY_DISK_CACHE);
 -              if (diskCachePath != null)
 -              {
 -                      File srtmDir = new File(diskCachePath, "srtm");
 -                      if (srtmDir.exists() && srtmDir.isDirectory() && srtmDir.canRead())
 -                      {
 -                              File srtmFile = new File(srtmDir, new File(inUrl.getFile()).getName());
 -                              if (srtmFile.exists() && srtmFile.isFile() && srtmFile.canRead()
 -                                      && srtmFile.length() > 400)
 -                              {
 -                                      // System.out.println("Lookup: Using file " + srtmFile.getAbsolutePath());
 -                                      // File found, use this one
 -                                      return new ZipInputStream(new FileInputStream(srtmFile));
 -                              }
 -                      }
 -              }
 -              // System.out.println("Lookup: Trying online: " + inUrl.toString());
 -              _hadToDownload = true;
 -              // MAYBE: Only download if we're in online mode?
 -              return new ZipInputStream(inUrl.openStream());
 -      }
 -
+       /**
+        * Given the height data read in from file, apply the given tile to all points
+        * in the track with missing altitude
+        * @param inTile tile being applied
+        * @param inHeights height data read in from file
+        * @param inOverwriteZeros true to overwrite zero altitude values
+        * @return number of altitudes found
+        */
 -      private int applySrtmTileToWholeTrack(SrtmTile inTile, int[] inHeights, boolean inOverwriteZeros)
++      private int applySrtmTimeToWholeTrack(SrtmTile inTile, int[] inHeights, int inRowSize, boolean inOverwriteZeros)
+       {
+               int numAltitudesFound = 0;
+               // Loop over all points in track, try to apply altitude from array
+               for (int p = 0; p < _track.getNumPoints(); p++)
+               {
+                       DataPoint point = _track.getPoint(p);
 -                      if (!point.hasAltitude()
 -                              || (inOverwriteZeros && point.getAltitude().getValue() == 0))
++                      if (needsAltitude(point, inOverwriteZeros))
+                       {
+                               if (new SrtmTile(point).equals(inTile))
+                               {
 -                                      double x = (point.getLongitude().getDouble() - inTile.getLongitude()) * 1200;
 -                                      double y = 1201 - (point.getLatitude().getDouble() - inTile.getLatitude()) * 1200;
 -                                      int idx1 = ((int)y)*1201 + (int)x;
++                                      double x = (point.getLongitude().getDouble() - inTile.getLongitude()) * (inRowSize - 1);
++                                      double y = inRowSize - (point.getLatitude().getDouble() - inTile.getLatitude()) * (inRowSize - 1);
++                                      int idx1 = ((int)y)*inRowSize + (int)x;
+                                       try
+                                       {
 -                                              int[] fouralts = {inHeights[idx1], inHeights[idx1+1], inHeights[idx1-1201], inHeights[idx1-1200]};
++                                              int[] fouralts = {inHeights[idx1], inHeights[idx1+1], inHeights[idx1-inRowSize], inHeights[idx1-inRowSize+1]};
+                                               int numVoids = (fouralts[0]==VOID_VAL?1:0) + (fouralts[1]==VOID_VAL?1:0)
+                                                       + (fouralts[2]==VOID_VAL?1:0) + (fouralts[3]==VOID_VAL?1:0);
+                                               // if (numVoids > 0) System.out.println(numVoids + " voids found");
+                                               double altitude = 0.0;
+                                               switch (numVoids)
+                                               {
 -                                                      case 0: altitude = bilinearInterpolate(fouralts, x, y); break;
 -                                                      case 1: altitude = bilinearInterpolate(fixVoid(fouralts), x, y); break;
 -                                                      case 2:
 -                                                      case 3: altitude = averageNonVoid(fouralts); break;
 -                                                      default: altitude = VOID_VAL;
++                                              case 0: altitude = bilinearInterpolate(fouralts, x, y); break;
++                                              case 1: altitude = bilinearInterpolate(fixVoid(fouralts), x, y); break;
++                                              case 2:
++                                              case 3: altitude = averageNonVoid(fouralts); break;
++                                              default: altitude = VOID_VAL;
+                                               }
+                                               // Special case for terrain tracks, don't interpolate voids yet
+                                               if (!_normalTrack && numVoids > 0) {
+                                                       altitude = VOID_VAL;
+                                               }
+                                               if (altitude != VOID_VAL)
+                                               {
+                                                       point.setFieldValue(Field.ALTITUDE, ""+altitude, false);
+                                                       // depending on settings, this value may have been added as feet, we need to force metres
+                                                       point.getAltitude().reset(new Altitude((int)altitude, UnitSetLibrary.UNITS_METRES));
+                                                       numAltitudesFound++;
+                                               }
+                                       }
+                                       catch (ArrayIndexOutOfBoundsException obe) {
 -                                              // System.err.println("lat=" + point.getLatitude().getDouble() + ", x=" + x + ", y=" + y + ", idx=" + idx1);
++                                              System.err.println("Point not in tile? lat=" + point.getLatitude().getDouble() + ", x=" + x + ", y=" + y + ", idx=" + idx1+"\n");
+                                       }
+                               }
+                       }
+               }
+               return numAltitudesFound;
+       }
        /**
         * Perform a bilinear interpolation on the given altitude array
         * @param inAltitudes array of four altitude values on corners of square (bl, br, tl, tr)
index d3386fe9d61acd073f7e3402fdf0976db18bdaee,d289b7828fca0086eacf58a6ce74275b1471c786..ed8474ca9b1d41978e9154e9d057c1f72aa20b44
@@@ -30,14 -30,14 +30,18 @@@ import tim.prune.data.Selection
  import tim.prune.data.Track;
  import tim.prune.data.TrackInfo;
  import tim.prune.function.ChooseSingleParameter;
+ import tim.prune.function.PasteCoordinateList;
+ import tim.prune.function.PasteCoordinates;
+ import tim.prune.function.PlusCodeFunction;
  import tim.prune.function.SearchOpenCachingDeFunction;
  import tim.prune.function.browser.UrlGenerator;
  import tim.prune.function.browser.WebMapFunction;
  import tim.prune.function.search.SearchMapillaryFunction;
 +import tim.prune.function.srtm.DownloadSrtmFunction;
 +import tim.prune.function.srtm.SrtmGl1Source;
 +import tim.prune.function.srtm.Srtm3Source;
 +import tim.prune.function.srtm.SrtmViewfinderSource;
+ import tim.prune.function.settings.SaveConfig;
  
  /**
   * Class to manage the menu bar and tool bar,
@@@ -78,10 -78,10 +82,11 @@@ public class MenuManager implements Dat
        private JMenuItem _selectEndItem = null;
        private JMenuItem _findWaypointItem = null;
        private JMenuItem _duplicatePointItem = null;
+       private JMenuItem _projectPointItem = null;
        private JMenuItem _reverseItem = null;
        private JMenuItem _addTimeOffsetItem = null;
        private JMenuItem _addAltitudeOffsetItem = null;
 +      private JMenuItem _removeAltitudesItem = null;
        private JMenuItem _mergeSegmentsItem = null;
        private JMenuItem _rearrangeWaypointsItem = null;
        private JMenuItem _splitSegmentsItem = null;
        private JMenu     _browserMapMenu = null;
        private JMenuItem _routingGraphHopperItem = null;
        private JMenuItem _chartItem = null;
-       private JMenuItem _getGpsiesItem = null;
-       private JMenuItem _uploadGpsiesItem = null;
        private JMenuItem _lookupSrtmItem = null;
 -      private JMenuItem _downloadSrtmItem = null;
 +      private JMenu     _downloadSrtmMenu = null;
        private JMenuItem _nearbyWikipediaItem = null;
        private JMenuItem _nearbyOsmPoiItem = null;
        private JMenuItem _showPeakfinderItem = null;
        private JMenuItem _downloadOsmItem = null;
        private JMenuItem _getWeatherItem = null;
        private JMenuItem _distanceItem = null;
-       private JMenuItem _fullRangeDetailsItem = null;
+       private JMenuItem _viewFullDetailsItem = null;
        private JMenuItem _estimateTimeItem = null;
        private JMenuItem _learnEstimationParams = null;
        private JMenuItem _autoplayTrack = null;
                // SRTM
                _lookupSrtmItem = makeMenuItem(FunctionLibrary.FUNCTION_LOOKUP_SRTM, false);
                onlineMenu.add(_lookupSrtmItem);
 -              _downloadSrtmItem = makeMenuItem(FunctionLibrary.FUNCTION_DOWNLOAD_SRTM, false);
 -              onlineMenu.add(_downloadSrtmItem);
 +              // Download SRTM sub-menu
 +              _downloadSrtmMenu = new JMenu(I18nManager.getText("function.downloadsrtm"));
 +              _downloadSrtmMenu.setEnabled(false);
 +              JMenuItem downloadStrmGl1Item = makeMenuItem(new DownloadSrtmFunction(_app, new SrtmGl1Source()));
 +              _downloadSrtmMenu.add(downloadStrmGl1Item);
 +              JMenuItem downloadStrmViewfinderItem = makeMenuItem(new DownloadSrtmFunction(_app, new SrtmViewfinderSource()));
 +              _downloadSrtmMenu.add(downloadStrmViewfinderItem);
 +              JMenuItem downloadStrm3Item = makeMenuItem(new DownloadSrtmFunction(_app, new Srtm3Source()));
 +              _downloadSrtmMenu.add(downloadStrm3Item);
 +              onlineMenu.add(_downloadSrtmMenu);
  
-               // Get gpsies tracks
-               _getGpsiesItem = makeMenuItem(FunctionLibrary.FUNCTION_GET_GPSIES, false);
-               onlineMenu.add(_getGpsiesItem);
-               // Upload to gpsies
-               _uploadGpsiesItem = makeMenuItem(FunctionLibrary.FUNCTION_UPLOAD_GPSIES, false);
-               onlineMenu.add(_uploadGpsiesItem);
                onlineMenu.addSeparator();
                // browser submenu
                _browserMapMenu = new JMenu(I18nManager.getText("menu.view.browser"));
                rangeMenu.add(_addTimeOffsetItem);
                _addAltitudeOffsetItem = makeMenuItem(FunctionLibrary.FUNCTION_ADD_ALTITUDE_OFFSET, false);
                rangeMenu.add(_addAltitudeOffsetItem);
 +              _removeAltitudesItem = makeMenuItem(FunctionLibrary.FUNCTION_REMOVE_ALTITUDES, false);
 +              rangeMenu.add(_removeAltitudesItem);
                _mergeSegmentsItem = new JMenuItem(I18nManager.getText("menu.range.mergetracksegments"));
                _mergeSegmentsItem.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
                // duplicate current point
                _duplicatePointItem = makeMenuItem(FunctionLibrary.FUNCTION_DUPLICATE_POINT, false);
                pointMenu.add(_duplicatePointItem);
+               // project current point
+               _projectPointItem = makeMenuItem(FunctionLibrary.FUNCTION_PROJECT_POINT, false);
+               pointMenu.add(_projectPointItem);
                // paste coordinates function
-               JMenuItem pasteCoordsItem = makeMenuItem(FunctionLibrary.FUNCTION_PASTE_COORDINATES);
+               JMenuItem pasteCoordsItem = makeMenuItem(new PasteCoordinates(_app));
                pointMenu.add(pasteCoordsItem);
+               JMenuItem pasteCoordsListItem = makeMenuItem(new PasteCoordinateList(_app));
+               pointMenu.add(pasteCoordsListItem);
+               // pluscodes function
+               JMenuItem plusCodeItem = makeMenuItem(new PlusCodeFunction(_app));
+               pointMenu.add(plusCodeItem);
                menubar.add(pointMenu);
  
                // Add view menu
                _distanceItem = makeMenuItem(FunctionLibrary.FUNCTION_DISTANCES, false);
                viewMenu.add(_distanceItem);
                // full range details
-               _fullRangeDetailsItem = makeMenuItem(FunctionLibrary.FUNCTION_FULL_RANGE_DETAILS, false);
-               viewMenu.add(_fullRangeDetailsItem);
+               _viewFullDetailsItem = makeMenuItem(FunctionLibrary.FUNCTION_FULL_DETAILS, false);
+               viewMenu.add(_viewFullDetailsItem);
                // estimate time
                _estimateTimeItem = makeMenuItem(FunctionLibrary.FUNCTION_ESTIMATE_TIME, false);
                viewMenu.add(_estimateTimeItem);
                settingsMenu.add(makeMenuItem(new ChooseSingleParameter(_app, FunctionLibrary.FUNCTION_SET_ALTITUDE_TOLERANCE)));
                // Set timezone
                settingsMenu.add(makeMenuItem(FunctionLibrary.FUNCTION_SET_TIMEZONE));
 +              // Set Earthdata authentication
 +              settingsMenu.add(makeMenuItem(FunctionLibrary.FUNCTION_SET_EARTHDATA_AUTH));
                settingsMenu.addSeparator();
                // Save configuration
                settingsMenu.add(makeMenuItem(FunctionLibrary.FUNCTION_SAVECONFIG));
                _autosaveSettingsCheckbox.setSelected(Config.getConfigBoolean(Config.KEY_AUTOSAVE_SETTINGS));
                _autosaveSettingsCheckbox.addActionListener(new ActionListener() {
                        public void actionPerformed(ActionEvent e) {
-                               Config.setConfigBoolean(Config.KEY_AUTOSAVE_SETTINGS, _autosaveSettingsCheckbox.isSelected());
+                               final boolean autosaveOn = _autosaveSettingsCheckbox.isSelected();
+                               Config.setConfigBoolean(Config.KEY_AUTOSAVE_SETTINGS, autosaveOn);
+                               // Maybe want to save config?
+                               new SaveConfig(_app).autosaveSwitched(autosaveOn);
                        }
                });
                settingsMenu.add(_autosaveSettingsCheckbox);
                _markRectangleItem.setEnabled(hasData);
                _markUphillLiftsItem.setEnabled(hasData && _track.hasAltitudeData());
                _deleteMarkedPointsItem.setEnabled(hasData && _track.hasMarkedPoints());
-               _rearrangeWaypointsItem.setEnabled(hasData && _track.hasTrackPoints() && _track.hasWaypoints());
+               _rearrangeWaypointsItem.setEnabled(hasData && _track.hasWaypoints() && _track.getNumPoints() > 1);
                final boolean hasSeveralTrackPoints = hasData && _track.hasTrackPoints() && _track.getNumPoints() > 3;
                _splitSegmentsItem.setEnabled(hasSeveralTrackPoints);
                _sewSegmentsItem.setEnabled(hasSeveralTrackPoints);
                _browserMapMenu.setEnabled(hasData);
                _distanceItem.setEnabled(hasData);
                _autoplayTrack.setEnabled(hasData && _track.getNumPoints() > 3);
-               _getGpsiesItem.setEnabled(hasData);
-               _uploadGpsiesItem.setEnabled(hasData && _track.hasTrackPoints());
                _lookupSrtmItem.setEnabled(hasData);
                _nearbyWikipediaItem.setEnabled(hasData);
                _nearbyOsmPoiItem.setEnabled(hasData);
                _getWeatherItem.setEnabled(hasData);
                _findWaypointItem.setEnabled(hasData && _track.hasWaypoints());
                // have we got a cache?
 -              _downloadSrtmItem.setEnabled(hasData && Config.getConfigString(Config.KEY_DISK_CACHE) != null);
 +              _downloadSrtmMenu.setEnabled(hasData && Config.getConfigString(Config.KEY_DISK_CACHE) != null);
                // have we got any timestamps?
                _deleteByDateItem.setEnabled(hasData && _track.hasData(Field.TIMESTAMP));
  
                _selectEndItem.setEnabled(hasPoint);
                _selectEndButton.setEnabled(hasPoint);
                _duplicatePointItem.setEnabled(hasPoint);
+               _projectPointItem.setEnabled(hasPoint);
                _showPeakfinderItem.setEnabled(hasPoint);
                _showGeohackItem.setEnabled(hasPoint);
                _searchOpencachingDeItem.setEnabled(hasPoint);
                _reverseItem.setEnabled(hasRange);
                _addTimeOffsetItem.setEnabled(hasRange);
                _addAltitudeOffsetItem.setEnabled(hasRange);
 +              _removeAltitudesItem.setEnabled(hasRange);
                _convertNamesToTimesItem.setEnabled(hasRange && _track.hasWaypoints());
                _deleteFieldValuesItem.setEnabled(hasRange);
-               _fullRangeDetailsItem.setEnabled(hasRange);
+               _viewFullDetailsItem.setEnabled(hasRange || hasPoint);
                _estimateTimeItem.setEnabled(hasRange);
                _learnEstimationParams.setEnabled(hasData && _track.hasTrackPoints() && _track.hasData(Field.TIMESTAMP)
                        && _track.hasAltitudeData());
index aec78a61f7703e3f3f97807c5347a04a91743708,38620e474ce6b78b7d72a06e2ce35303d7acbd3d..56711a3093bbb69ee6e6f64dfed96b1342c84771
@@@ -7,7 -7,7 +7,7 @@@ import tim.prune.gui.map.MapUtils
  /**
   * Class to provide access to current viewport
   * The point of this class is to decouple the view from the MapCanvas object
-  * so that when the GetGpsies function needs to know the area currently viewed, it doesn't
+  * so that when a search function needs to know the area currently viewed, it doesn't
   * need to have a direct connection to the MapCanvas.  Instead it asks the App for the viewport,
   * which is then able to get the map position from the MapCanvas.
   * I'm still not sure whether this is ugly or not, but it's more efficient than constantly listening.
@@@ -40,12 -40,4 +40,12 @@@ public class Viewpor
                double maxLon = MapUtils.getLongitudeFromX(mapPosition.getXFromPixels(width, width));
                return new double[] {minLat, minLon, maxLat, maxLon};
        }
 +
 +      /**
 +       * Recentre the viewport on the data
 +       */
 +      public void recentreViewport()
 +      {
 +              _mapCanvas.zoomToFit();
 +      }
  }
index ad38d8182700cf49a1d5055f66bbb4ec8b56a20e,d305d1c4f7bd3b9dae59066bf4f32423b9426c39..d3a1de5d8afcc1c39cff44b54527a4bf92d35d18
@@@ -92,7 -92,7 +92,7 @@@ public class MapCanvas extends JPanel i
        WpIconDefinition _waypointIconDefinition = null;
  
        /** Constant for click sensitivity when selecting nearest point */
 -      private static final int CLICK_SENSITIVITY = 10;
 +      private static final int CLICK_SENSITIVITY = 30;
        /** Constant for pan distance from key presses */
        private static final int PAN_DISTANCE = 20;
        /** Constant for pan distance from autopan */
  
                // add control panels to this one
                setLayout(new BorderLayout());
 -              _topPanel.setVisible(false);
 -              _sidePanel.setVisible(false);
 +              _topPanel.setVisible(true);
 +              _sidePanel.setVisible(true);
                add(_topPanel, BorderLayout.NORTH);
                add(_sidePanel, BorderLayout.WEST);
                add(_scaleBar, BorderLayout.SOUTH);
        /**
         * Zoom to fit the current data area
         */
 -      private void zoomToFit()
 +      public void zoomToFit()
        {
 +              int maxZoom = (_track.getNumPoints() == 0)?2:_tileManager.getMaxZoomLevel();
                _latRange = _track.getLatRange();
                _lonRange = _track.getLonRange();
                _xRange = new DoubleRange(MapUtils.getXFromLongitude(_lonRange.getMinimum()),
                        MapUtils.getXFromLongitude(_lonRange.getMaximum()));
                _yRange = new DoubleRange(MapUtils.getYFromLatitude(_latRange.getMinimum()),
                        MapUtils.getYFromLatitude(_latRange.getMaximum()));
 -              _mapPosition.zoomToXY(_xRange.getMinimum(), _xRange.getMaximum(), _yRange.getMinimum(), _yRange.getMaximum(),
 -                      getWidth(), getHeight());
 +              _mapPosition.zoomToXY(
 +                      _xRange.getMinimum(), _xRange.getMaximum(),
 +                      _yRange.getMinimum(), _yRange.getMaximum(),
 +                      getWidth(), getHeight(), maxZoom);
        }
  
  
                if (_mapImage != null && (_mapImage.getWidth() != getWidth() || _mapImage.getHeight() != getHeight())) {
                        _mapImage = null;
                }
 -              if (_track.getNumPoints() > 0)
 +              // Check for autopan if enabled / necessary
 +              if (_autopanCheckBox.isSelected())
                {
 -                      // Check for autopan if enabled / necessary
 -                      if (_autopanCheckBox.isSelected())
 +                      int selectedPoint = _selection.getCurrentPointIndex();
 +                      if (selectedPoint >= 0 && _dragFromX == -1 && selectedPoint != _prevSelectedPoint)
                        {
 -                              int selectedPoint = _selection.getCurrentPointIndex();
 -                              if (selectedPoint >= 0 && _dragFromX == -1 && selectedPoint != _prevSelectedPoint)
 -                              {
 -                                      autopanToPoint(selectedPoint);
 -                              }
 -                              _prevSelectedPoint = selectedPoint;
 +                              autopanToPoint(selectedPoint);
                        }
 +                      _prevSelectedPoint = selectedPoint;
 +              }
  
 -                      // Draw the map contents if necessary
 -                      if (_mapImage == null || _recalculate)
 -                      {
 -                              paintMapContents();
 -                              _scaleBar.updateScale(_mapPosition.getZoom(), _mapPosition.getYFromPixels(0, 0));
 -                      }
 -                      // Draw the prepared image onto the panel
 -                      if (_mapImage != null) {
 -                              inG.drawImage(_mapImage, 0, 0, getWidth(), getHeight(), null);
 -                      }
 +              // Draw the map contents if necessary
 +              if (_mapImage == null || _recalculate)
 +              {
 +                      paintMapContents();
 +                      _scaleBar.updateScale(_mapPosition.getZoom(), _mapPosition.getYFromPixels(0, 0));
 +              }
 +              // Draw the prepared image onto the panel
 +              if (_mapImage != null) {
 +                      inG.drawImage(_mapImage, 0, 0, getWidth(), getHeight(), null);
 +              }
  
 -                      switch (_drawMode)
 -                      {
 -                              case MODE_DRAG_POINT:
 -                                      drawDragLines(inG, _selection.getCurrentPointIndex()-1, _selection.getCurrentPointIndex()+1);
 -                                      break;
 +              switch (_drawMode)
 +              {
 +                      case MODE_DRAG_POINT:
 +                              drawDragLines(inG, _selection.getCurrentPointIndex()-1, _selection.getCurrentPointIndex()+1);
 +                              break;
  
 -                              case MODE_CREATE_MIDPOINT:
 -                                      drawDragLines(inG, _clickedPoint-1, _clickedPoint);
 -                                      break;
 +                      case MODE_CREATE_MIDPOINT:
 +                              drawDragLines(inG, _clickedPoint-1, _clickedPoint);
 +                              break;
  
 -                              case MODE_ZOOM_RECT:
 -                              case MODE_MARK_RECTANGLE:
 -                                      if (_dragFromX != -1 && _dragFromY != -1)
 -                                      {
 -                                              // Draw the zoom rectangle if necessary
 -                                              inG.setColor(Color.RED);
 -                                              inG.drawLine(_dragFromX, _dragFromY, _dragFromX, _dragToY);
 -                                              inG.drawLine(_dragFromX, _dragFromY, _dragToX, _dragFromY);
 -                                              inG.drawLine(_dragToX, _dragFromY, _dragToX, _dragToY);
 -                                              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));
 -                                      int prevIndex = _track.getNumPoints()-1;
 -                                      int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(prevIndex));
 -                                      int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(prevIndex));
 -                                      inG.drawLine(px, py, _dragToX, _dragToY);
 -                                      break;
 -                      }
 -              }
 -              else
 -              {
 -                      inG.setColor(Config.getColourScheme().getColour(ColourScheme.IDX_BACKGROUND));
 -                      inG.fillRect(0, 0, getWidth(), getHeight());
 -                      inG.setColor(COLOR_MESSAGES);
 -                      inG.drawString(I18nManager.getText("display.nodata"), 50, getHeight()/2);
 -                      _scaleBar.updateScale(-1, 0);
 +                      case MODE_ZOOM_RECT:
 +                      case MODE_MARK_RECTANGLE:
 +                              if (_dragFromX != -1 && _dragFromY != -1)
 +                              {
 +                                      // Draw the zoom rectangle if necessary
 +                                      inG.setColor(Color.RED);
 +                                      inG.drawLine(_dragFromX, _dragFromY, _dragFromX, _dragToY);
 +                                      inG.drawLine(_dragFromX, _dragFromY, _dragToX, _dragFromY);
 +                                      inG.drawLine(_dragToX, _dragFromY, _dragToX, _dragToY);
 +                                      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));
 +                              int prevIndex = _track.getNumPoints()-1;
 +                              int px = getWidth() / 2 + _mapPosition.getXFromCentre(_track.getX(prevIndex));
 +                              int py = getHeight() / 2 + _mapPosition.getYFromCentre(_track.getY(prevIndex));
 +                              inG.drawLine(px, py, _dragToX, _dragToY);
 +                              break;
                }
                // Draw slider etc on top
                paintChildren(inG);
                                                final double pointSeparationSqd = (prevX-px) * (prevX-px) + (prevY-py) * (prevY-py);
                                                if (pointSeparationSqd > pointSeparationForArrowsSqd)
                                                {
-                                                       final double midX = (prevX + px) / 2;
-                                                       final double midY = (prevY + py) / 2;
+                                                       final double midX = (prevX + px) / 2.0;
+                                                       final double midY = (prevY + py) / 2.0;
                                                        final boolean midPointVisible = midX >= 0 && midX < winWidth && midY >= 0 && midY < winHeight;
                                                        if (midPointVisible)
                                                        {
         */
        public void mouseClicked(MouseEvent inE)
        {
 -              if (_track != null && _track.getNumPoints() > 0)
 +              // select point if it's a left-click
 +              if (!inE.isMetaDown())
                {
 -                      // select point if it's a left-click
 -                      if (!inE.isMetaDown())
 +                      if (inE.getClickCount() == 1)
                        {
 -                              if (inE.getClickCount() == 1)
 +                              // single click
 +                              if (_drawMode == MODE_DEFAULT)
                                {
 -                                      // single click
 -                                      if (_drawMode == MODE_DEFAULT)
 +                                      int pointIndex = _clickedPoint;
 +                                      if (pointIndex == INDEX_UNKNOWN)
                                        {
 -                                              int pointIndex = _clickedPoint;
 -                                              if (pointIndex == INDEX_UNKNOWN)
 -                                              {
 -                                                      // index hasn't been calculated yet
 -                                                      pointIndex = _track.getNearestPointIndex(
 -                                                       _mapPosition.getXFromPixels(inE.getX(), getWidth()),
 -                                                       _mapPosition.getYFromPixels(inE.getY(), getHeight()),
 -                                                       _mapPosition.getBoundsFromPixels(CLICK_SENSITIVITY), false);
 -                                              }
 -                                              // Extend selection for shift-click
 -                                              if (inE.isShiftDown()) {
 -                                                      _trackInfo.extendSelection(pointIndex);
 -                                              }
 -                                              else {
 -                                                      _trackInfo.selectPoint(pointIndex);
 -                                              }
 +                                              // index hasn't been calculated yet
 +                                              pointIndex = _track.getNearestPointIndex(
 +                                                      _mapPosition.getXFromPixels(inE.getX(), getWidth()),
 +                                                      _mapPosition.getYFromPixels(inE.getY(), getHeight()),
 +                                                      _mapPosition.getBoundsFromPixels(CLICK_SENSITIVITY), false);
                                        }
 -                                      else if (_drawMode == MODE_DRAW_POINTS_START)
 -                                      {
 -                                              _app.createPoint(createPointFromClick(inE.getX(), inE.getY()));
 -                                              _dragToX = inE.getX();
 -                                              _dragToY = inE.getY();
 -                                              _drawMode = MODE_DRAW_POINTS_CONT;
 +                                      // Extend selection for shift-click
 +                                      if (inE.isShiftDown()) {
 +                                              _trackInfo.extendSelection(pointIndex);
                                        }
 -                                      else if (_drawMode == MODE_DRAW_POINTS_CONT)
 -                                      {
 -                                              DataPoint point = createPointFromClick(inE.getX(), inE.getY());
 -                                              _app.createPoint(point, false); // not a new segment
 +                                      else {
 +                                              _trackInfo.selectPoint(pointIndex);
                                        }
                                }
 -                              else if (inE.getClickCount() == 2)
 +                              else if (_drawMode == MODE_DRAW_POINTS_START)
                                {
 -                                      // double click
 -                                      if (_drawMode == MODE_DEFAULT) {
 -                                              panMap(inE.getX() - getWidth()/2, inE.getY() - getHeight()/2);
 -                                              zoomIn();
 -                                      }
 -                                      else if (_drawMode == MODE_DRAW_POINTS_START || _drawMode == MODE_DRAW_POINTS_CONT) {
 -                                              _drawMode = MODE_DEFAULT;
 -                                      }
 +                                      _app.createPoint(createPointFromClick(inE.getX(), inE.getY()));
 +                                      _dragToX = inE.getX();
 +                                      _dragToY = inE.getY();
 +                                      _drawMode = MODE_DRAW_POINTS_CONT;
 +                              }
 +                              else if (_drawMode == MODE_DRAW_POINTS_CONT)
 +                              {
 +                                      DataPoint point = createPointFromClick(inE.getX(), inE.getY());
 +                                      _app.createPoint(point, false); // not a new segment
                                }
                        }
 -                      else
 +                      else if (inE.getClickCount() == 2)
                        {
 -                              // show the popup menu for right-clicks
 -                              _popupMenuX = inE.getX();
 -                              _popupMenuY = inE.getY();
 -                              _popup.show(this, _popupMenuX, _popupMenuY);
 +                              // double click
 +                              if (_drawMode == MODE_DEFAULT) {
 +                                      panMap(inE.getX() - getWidth()/2, inE.getY() - getHeight()/2);
 +                                      zoomIn();
 +                              }
 +                              else if (_drawMode == MODE_DRAW_POINTS_START || _drawMode == MODE_DRAW_POINTS_CONT) {
 +                                      _drawMode = MODE_DEFAULT;
 +                              }
                        }
                }
 +              else
 +              {
 +                      // show the popup menu for right-clicks
 +                      _popupMenuX = inE.getX();
 +                      _popupMenuY = inE.getY();
 +                      _popup.show(this, _popupMenuX, _popupMenuY);
 +              }
                // Reset app mode
                _app.setCurrentMode(App.AppMode.NORMAL);
                if (_drawMode == MODE_MARK_RECTANGLE) _drawMode = MODE_DEFAULT;
                        }
                }
                repaint();
 -              // enable or disable components
 -              boolean hasData = _track.getNumPoints() > 0;
 -              _topPanel.setVisible(hasData);
 -              _sidePanel.setVisible(hasData);
                // grab focus for the key presses
                this.requestFocus();
        }
index d1e0047a2bc2624284308fad0f4ab1ea9df2dc09,8c2c6eed9e3f7e70ccf251ba91e01ebe4f550be9..b4a60c88b607a10cee8737f7ce80bd4c3b701e89
@@@ -70,19 -70,10 +70,19 @@@ public class MapTileManager implements 
         * @return true if zoom is too high for tiles
         */
        public boolean isOverzoomed()
 +      {
 +              return _zoom > getMaxZoomLevel();
 +      }
 +
 +      /**
 +       * @return the maximum useable zoom level for tiles
 +       */
 +      public int getMaxZoomLevel()
        {
                // Ask current map source what maximum zoom is
                int maxZoom = (_mapSource == null?0:_mapSource.getMaxZoomLevel());
 -              return (_zoom > maxZoom);
 +              return maxZoom;
 +
        }
  
        /**
                        tempCache = _tempCaches[inLayer]; // Should probably guard array indexes here
                        tileImage = tempCache.getTile(inX, inY);
                        if (tileImage != null) {
+                               //System.out.println("Got tile from memory: " + inX + ", " + inY);
                                return tileImage;
                        }
                }
                        try
                        {
                                URL tileUrl = new URL(_mapSource.makeURL(inLayer, _zoom, inX, inY));
-                               //System.out.println("Trying to fetch: " + tileUrl);
                                if (useDisk)
                                {
                                        DiskTileCacher.saveTile(tileUrl, diskCachePath,
index 35ac13bbe9f4feb91555c3f099f84c2b26d567c3,dbc4be4a59d3cff49a9dbea409a07beab1dca1cc..fe5210cfd1c21dd0b41d17d9cf88b8f8448e5780
@@@ -94,16 -94,17 +94,18 @@@ function.interpolate=Interpolate point
  function.deletebydate=Delete points by date
  function.addtimeoffset=Add time offset
  function.addaltitudeoffset=Add altitude offset
 +function.removealtitudes=Remove altitudes
  function.findwaypoint=Find waypoint
  function.rearrangewaypoints=Rearrange waypoints
  function.convertnamestotimes=Convert waypoint names to times
  function.deletefieldvalues=Delete field values
- function.pastecoordinates=Enter new coordinates
+ function.pastecoordinates=Enter point coordinates
+ function.pastecoordinatelist=Enter list of coordinates
+ function.enterpluscode=Enter pluscode
  function.charts=Charts
  function.show3d=Three-D view
  function.distances=Distances
- function.fullrangedetails=Full range details
+ function.viewfulldetails=Full details
  function.estimatetime=Estimate time
  function.learnestimationparams=Learn time estimation parameters
  function.autoplay=Autoplay track
@@@ -111,14 -112,8 +113,12 @@@ function.selectsegment=Select current s
  function.splitsegments=Split track into segments
  function.sewsegments=Sew track segments together
  function.createmarkerwaypoints=Create marker waypoints
- function.getgpsies=Get Gpsies tracks
- function.uploadgpsies=Upload track to Gpsies
  function.lookupsrtm=Get altitudes from SRTM
  function.downloadsrtm=Download SRTM tiles
 +function.downloadsrtm.SRTMGL1_v003=SRTM 1 arc-second tiles
 +function.downloadsrtm.SRTMGL1_v003.needsetup=An Earthdata account is necessary to download SRTM 1 arc-second tiles
 +function.downloadsrtm.SRTM3_v21=SRTM 3 arc-second tiles
 +function.downloadsrtm.SRTM_Viewfinder=Viewfinderpanoramas.org data
  function.getwikipedia=Get nearby Wikipedia articles
  function.searchwikipedianames=Search Wikipedia by name
  function.searchosmpois=Get nearby OSM points
@@@ -126,6 -121,7 +126,7 @@@ function.searchopencachingde=Search Ope
  function.mapillary=Search for photos in Mapillary
  function.downloadosm=Download OSM data for area
  function.duplicatepoint=Duplicate point
+ function.projectpoint=Project point
  function.connecttopoint=Connect to point
  function.disconnectfrompoint=Disconnect from point
  function.removephoto=Remove photo
@@@ -155,7 -151,6 +156,7 @@@ function.managetilecache=Manage tile ca
  function.getweatherforecast=Get weather forecast
  function.setaltitudetolerance=Set altitude tolerance
  function.selecttimezone=Set timezone
 +function.setearthdataauthentication=Set Earthdata authentication
  
  # Dialogs
  dialog.exit.confirm.title=Exit GpsPrune
@@@ -308,7 -303,6 +309,7 @@@ dialog.pointnameedit.lowercase=lower ca
  dialog.pointnameedit.titlecase=Title Case
  dialog.addtimeoffset.add=Add time
  dialog.addtimeoffset.subtract=Subtract time
 +dialog.addtimeoffset.1024week=1024-week block
  dialog.addtimeoffset.days=Days
  dialog.addtimeoffset.hours=Hours
  dialog.addtimeoffset.minutes=Minutes
@@@ -377,19 -371,6 +378,6 @@@ dialog.gpsies.column.length=Lengt
  dialog.gpsies.description=Description
  dialog.gpsies.nodescription=No description
  dialog.gpsies.nonefound=No tracks found
- dialog.gpsies.username=Gpsies username
- dialog.gpsies.password=Gpsies password
- dialog.gpsies.keepprivate=Keep track private
- dialog.gpsies.confirmopenpage=Open the web page for the uploaded track?
- dialog.gpsies.activities=Activity types
- dialog.gpsies.activity.trekking=Hiking
- dialog.gpsies.activity.walking=Walking
- dialog.gpsies.activity.jogging=Running
- dialog.gpsies.activity.biking=Cycling
- dialog.gpsies.activity.motorbiking=Motorbiking
- dialog.gpsies.activity.snowshoe=Snowshoeing
- dialog.gpsies.activity.sailing=Sailing
- dialog.gpsies.activity.skating=Skating
  dialog.mapillary.nonefound=No photos found
  dialog.wikipedia.column.name=Article name
  dialog.wikipedia.column.distance=Distance
@@@ -457,6 -438,10 +445,10 @@@ dialog.deletemarked.nonefound=No data p
  dialog.pastecoordinates.desc=Enter or paste the coordinates here
  dialog.pastecoordinates.coords=Coordinates
  dialog.pastecoordinates.nothingfound=Please check the coordinates and try again
+ dialog.pastecoordinatelist.desc=Enter the coordinates for the new points with one point per line
+ dialog.pluscode.desc=Enter or paste the pluscode here
+ dialog.pluscode.code=Pluscode
+ dialog.pluscode.nothingfound=Please check the code and try again
  dialog.help.help=Please see\n https://gpsprune.activityworkshop.net/\nfor more information and tips,\nincluding a PDF user guide you can buy.
  dialog.about.version=Version
  dialog.about.build=Build
@@@ -496,25 -481,9 +488,9 @@@ dialog.keys.intro=You can use the follo
  dialog.keys.keylist=<table><tr><td>Arrow keys</td><td>Pan map left right, up, down</td></tr><tr><td>Ctrl + left, right arrow</td><td>Select previous or next point</td></tr><tr><td>Ctrl + up, down arrow</td><td>Zoom in or out</td></tr><tr><td>Ctrl + PgUp, PgDown</td><td>Select previous, next segment</td></tr><tr><td>Ctrl + Home, End</td><td>Select first, last point</td></tr><tr><td>Del</td><td>Delete current point</td></tr></table>
  dialog.keys.normalmodifier=Ctrl
  dialog.keys.macmodifier=Command
- dialog.saveconfig.desc=The following settings can be saved to a configuration file :
- dialog.saveconfig.prune.trackdirectory=Track directory
- dialog.saveconfig.prune.photodirectory=Photo directory
- dialog.saveconfig.prune.languagecode=Language code (EN)
- dialog.saveconfig.prune.languagefile=Language file
- dialog.saveconfig.prune.gpsdevice=GPS device
- dialog.saveconfig.prune.gpsformat=GPS format
- dialog.saveconfig.prune.povrayfont=Povray font
- dialog.saveconfig.prune.gnuplotpath=Path to gnuplot
- dialog.saveconfig.prune.gpsbabelpath=Path to gpsbabel
- 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 size
- dialog.saveconfig.prune.colourscheme=Colour scheme
- dialog.saveconfig.prune.linewidth=Line width
- dialog.saveconfig.prune.kmltrackcolour=KML track colour
- dialog.saveconfig.prune.autosavesettings=Autosave settings
+ dialog.paths.prune.gnuplotpath=Path to gnuplot
+ dialog.paths.prune.gpsbabelpath=Path to gpsbabel
+ dialog.paths.prune.exiftoolpath=Path to exiftool
  dialog.setpaths.intro=If you need to, you can choose the paths to the external applications:
  dialog.setpaths.found=Path found?
  dialog.addaltitude.noaltitudes=The selected range does not contain altitudes
@@@ -581,12 -550,10 +557,15 @@@ dialog.displaysettings.wpicon.pin=Boar
  dialog.displaysettings.size.small=Small
  dialog.displaysettings.size.medium=Medium
  dialog.displaysettings.size.large=Large
+ dialog.displaysettings.windowstyle=Window style (requires restart)
+ dialog.displaysettings.windowstyle.default=Default
+ dialog.displaysettings.windowstyle.nimbus=Nimbus
  dialog.downloadosm.desc=Confirm to download the raw OSM data for the specified area:
 +dialog.earthdataauth.intro=<p>Configure username and password to access your NASA Earthdata login account.</p><p> Create an account at <tt>https://urs.earthdata.nasa.gov/users/new</tt>.</p>
 +dialog.earthdataauth.user=Username
 +dialog.earthdataauth.password=Password
 +dialog.earthdataauth.authaccepted=Username and password accepted
 +dialog.earthdataauth.authrejected=Username and password rejected
  dialog.searchwikipedianames.search=Search for:
  dialog.weather.location=Location
  dialog.weather.update=Forecast updated
@@@ -628,6 -595,12 +607,12 @@@ dialog.autoplay.usetimestamps=Use poin
  dialog.autoplay.rewind=Back to beginning
  dialog.autoplay.pause=Pause
  dialog.autoplay.play=Play
+ dialog.markers.halves=Halfway points
+ dialog.markers.half.distance=Half distance
+ dialog.markers.half.climb=Half climb
+ dialog.markers.half.descent=Half descent
+ dialog.projectpoint.desc=Enter the direction and distance to project this point
+ dialog.projectpoint.bearing=Bearing (degrees from N)
  
  # 3d window
  dialog.3d.title=GpsPrune Three-d view
@@@ -644,13 -617,12 +629,13 @@@ confirm.mergetracksegments=Track segmen
  confirm.reverserange=Range reversed
  confirm.addtimeoffset=Time offset added
  confirm.addaltitudeoffset=Altitude offset added
 +confirm.removealtitudes=Altitudes removed
  confirm.rearrangewaypoints=Waypoints rearranged
  confirm.rearrangephotos=Photos rearranged
  confirm.splitsegments=%d segment splits were made
  confirm.sewsegments=%d segment joins were made
  confirm.cutandmove=Selection moved
- confirm.interpolate=Points added
+ confirm.pointsadded=%d points added
  confirm.convertnamestotimes=Waypoint names converted
  confirm.saveexif.ok=Saved %d photo files
  confirm.undo.single=operation undone
@@@ -706,7 -678,6 +691,6 @@@ button.selectall=Select al
  button.selectnone=Select none
  button.preview=Preview
  button.load=Load
- button.upload=Upload
  button.guessfields=Guess fields
  button.showwebpage=Show webpage
  button.check=Check
@@@ -765,9 -736,6 +749,9 @@@ details.range.gradient=Gradien
  details.lists.waypoints=Waypoints
  details.lists.photos=Photos
  details.lists.audio=Audio
 +details.lists.segments=Segments
 +details.lists.segments.label=Segment #
 +details.lists.segments.to=to
  details.photodetails=Photo details
  details.nophoto=No photo selected
  details.photo.loading=Loading
@@@ -783,6 -751,7 +767,7 @@@ map.overzoom=No maps available at this 
  # Field names
  fieldname.latitude=Latitude
  fieldname.longitude=Longitude
+ fieldname.coordinates=Coordinates
  fieldname.altitude=Altitude
  fieldname.timestamp=Time
  fieldname.time=Time
@@@ -797,6 -766,7 +782,7 @@@ fieldname.duration=Duratio
  fieldname.speed=Speed
  fieldname.verticalspeed=Vertical speed
  fieldname.description=Description
+ fieldname.comment=Comment
  fieldname.mediafilename=Filename
  
  # Measurement units
@@@ -868,7 -838,6 +854,7 @@@ undo.splitsegments=split track segment
  undo.sewsegments=sew track segments
  undo.addtimeoffset=add time offset
  undo.addaltitudeoffset=add altitude offset
 +undo.removealtitudes=remove altitudes
  undo.rearrangewaypoints=rearrange waypoints
  undo.cutandmove=move section
  undo.connect=connect
@@@ -897,6 -866,7 +883,7 @@@ error.load.nopoints=No coordinate infor
  error.load.unknownxml=Unrecognised xml format:
  error.load.noxmlinzip=No xml file found inside zip file
  error.load.othererror=Error reading file:
+ error.load.nopointsintext=No coordinate information found
  error.jpegload.dialogtitle=Error loading photos
  error.jpegload.nofilesfound=No files found
  error.jpegload.nojpegsfound=No jpeg files found