From: Frédéric Perrin Date: Sun, 5 Jan 2020 14:40:47 +0000 (+0000) Subject: Merge branch 'add-1024-week-offset' into fp-integration X-Git-Tag: v19.2.fp4 X-Git-Url: http://gitweb.fperrin.net/?p=GpsPrune.git;a=commitdiff_plain;h=22d91cbabcff03a472489640b7f46b3dbadc8e74;hp=026186a3da84e3eabbaa531083b33f53091afbaa Merge branch 'add-1024-week-offset' into fp-integration --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e9e794 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.class +*.jar diff --git a/buildtools/build.sh b/buildtools/build.sh old mode 100644 new mode 100755 index b38e2ce..c7fcf3d --- a/buildtools/build.sh +++ b/buildtools/build.sh @@ -1,3 +1,4 @@ +set -e # Build script # Version number PRUNENAME=gpsprune_19.2 diff --git a/src/tim/prune/App.java b/src/tim/prune/App.java index 3a77858..317064d 100644 --- a/src/tim/prune/App.java +++ b/src/tim/prune/App.java @@ -51,6 +51,7 @@ public class App { // Instance variables private JFrame _frame = null; + private String _titlePrefix = null; private Track _track = null; private TrackInfo _trackInfo = null; private int _lastSavePosition = 0; @@ -79,6 +80,7 @@ public class App public App(JFrame inFrame) { _frame = inFrame; + _titlePrefix = _frame.getTitle(); _undoStack = new UndoStack(); _track = new Track(); _trackInfo = new TrackInfo(_track); @@ -454,6 +456,22 @@ public class App } + /** + * 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 */ @@ -759,6 +777,10 @@ public class App + " '" + 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 @@ -1001,4 +1023,16 @@ public class App public void setCurrentMode(AppMode inMode) { _appMode = inMode; } + + /** Update main window title **/ + public void updateTitle() { + ArrayList filenames = _trackInfo.getFileInfo().getFilenames(); + if (filenames.size() > 0) { + _frame.setTitle(_titlePrefix + ": " + String.join(", ", filenames)); + } + else + { + _frame.setTitle(_titlePrefix); + } + } } diff --git a/src/tim/prune/FunctionLibrary.java b/src/tim/prune/FunctionLibrary.java index 0441826..4be286e 100644 --- a/src/tim/prune/FunctionLibrary.java +++ b/src/tim/prune/FunctionLibrary.java @@ -28,6 +28,7 @@ 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; @@ -56,12 +57,12 @@ 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; @@ -103,13 +104,13 @@ public abstract class FunctionLibrary 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; @@ -146,6 +147,7 @@ public abstract class FunctionLibrary 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; @@ -183,13 +185,13 @@ public abstract class FunctionLibrary 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); @@ -227,6 +229,7 @@ public abstract class FunctionLibrary 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); diff --git a/src/tim/prune/config/Config.java b/src/tim/prune/config/Config.java index 84e5fd7..3939c00 100644 --- a/src/tim/prune/config/Config.java +++ b/src/tim/prune/config/Config.java @@ -105,6 +105,8 @@ public abstract class Config 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 */ diff --git a/src/tim/prune/data/DataPoint.java b/src/tim/prune/data/DataPoint.java index 84f4800..01e9975 100644 --- a/src/tim/prune/data/DataPoint.java +++ b/src/tim/prune/data/DataPoint.java @@ -354,6 +354,16 @@ public class DataPoint } } + /** + * Remove altitude from point + */ + public void removeAltitude() + { + _altitude = Altitude.NONE; + _fieldValues[_fieldList.getFieldIndex(Field.ALTITUDE)] = _altitude.getStringValue(null); + setModified(false); + } + /** * Reset the altitude to the previous value (by an undo) * @param inClone altitude object cloned from earlier diff --git a/src/tim/prune/data/FileInfo.java b/src/tim/prune/data/FileInfo.java index 41900cb..7eaac8f 100644 --- a/src/tim/prune/data/FileInfo.java +++ b/src/tim/prune/data/FileInfo.java @@ -74,6 +74,19 @@ public class FileInfo return ""; } + /** + * @return The source names + */ + public ArrayList getFilenames() + { + ArrayList filenames = new ArrayList(); + for (SourceInfo source : _sources) + { + filenames.add(source.getName()); + } + return filenames; + } + /** * @param inIndex index number, starting from zero * @return source info object diff --git a/src/tim/prune/data/Track.java b/src/tim/prune/data/Track.java index f45854a..ef099d4 100644 --- a/src/tim/prune/data/Track.java +++ b/src/tim/prune/data/Track.java @@ -377,6 +377,31 @@ public class Track } + /** + * Remove altitudes from the specified range + * @param inStart start of range + * @param inEnd end of range + */ + public boolean removeAltitudes(int inStart, int inEnd) + { + // sanity check + if (inStart < 0 || inEnd < 0 || inStart >= inEnd || inEnd >= _numPoints) { + return false; + } + + boolean anyRemoved = false; + for (int i=inStart; i<=inEnd; i++) + { + DataPoint p = _dataPoints[i]; + if (p != null && p.hasAltitude()) + { + p.removeAltitude(); + anyRemoved = true; + } + } + return anyRemoved; + } + /** * Interleave all waypoints by each nearest track point * @return true if successful, false if no change diff --git a/src/tim/prune/function/RemoveAltitudes.java b/src/tim/prune/function/RemoveAltitudes.java new file mode 100644 index 0000000..1c8b05c --- /dev/null +++ b/src/tim/prune/function/RemoveAltitudes.java @@ -0,0 +1,60 @@ +package tim.prune.function; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +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.JTextField; + +import tim.prune.App; +import tim.prune.GenericFunction; +import tim.prune.I18nManager; +import tim.prune.config.Config; +import tim.prune.data.Field; +import tim.prune.data.Unit; +import tim.prune.data.UnitSetLibrary; + +/** + * Class to provide the function to add remove the altitude from points in a + * track range + */ +public class RemoveAltitudes extends GenericFunction +{ + /** + * Constructor + * @param inApp application object for callback + */ + public RemoveAltitudes(App inApp) + { + super(inApp); + } + + /** Get the name key */ + public String getNameKey() { + return "function.removealtitudes"; + } + + /** + * Begin the function + */ + public void begin() + { + int selStart = _app.getTrackInfo().getSelection().getStart(); + int selEnd = _app.getTrackInfo().getSelection().getEnd(); + if (!_app.getTrackInfo().getTrack().hasData(Field.ALTITUDE, selStart, selEnd)) + { + _app.showErrorMessage(getNameKey(), "dialog.addaltitude.noaltitudes"); + return; + } + _app.removeAltitudes(selStart, selEnd); + } +} diff --git a/src/tim/prune/function/edit/PointEditor.java b/src/tim/prune/function/edit/PointEditor.java index c66201b..546a65e 100644 --- a/src/tim/prune/function/edit/PointEditor.java +++ b/src/tim/prune/function/edit/PointEditor.java @@ -307,10 +307,23 @@ public class PointEditor if (_model.getChanged(i)) { Field field = fieldList.getField(i); + if (field == field.WAYPT_NAME) { + if (wasNameAdded(_model.getValue(i))) { + _app.createPoint(_point.clonePoint()); + } + } editList.addEdit(new FieldEdit(field, _model.getValue(i))); undoList.addEdit(new FieldEdit(field, _point.getFieldValue(field))); } } _app.completePointEdit(editList, undoList); } + + private boolean wasNameAdded(String newName) + { + String prevName = _point.getWaypointName(); + boolean prevNull = (prevName == null || prevName.equals("")); + boolean newNull = (newName == null || newName.equals("")); + return (prevNull && !newNull); + } } diff --git a/src/tim/prune/function/edit/PointNameEditor.java b/src/tim/prune/function/edit/PointNameEditor.java index 53e1f83..ca5771d 100644 --- a/src/tim/prune/function/edit/PointNameEditor.java +++ b/src/tim/prune/function/edit/PointNameEditor.java @@ -206,7 +206,14 @@ public class PointNameEditor extends GenericFunction // Check whether name has really changed if (hasNameChanged()) { - // Make lists for edit and undo, and add the changed field + // If a new name has been added, changing the point + // from trackpoint to waypoint, duplicate it + if (wasNameAdded()) + { + _app.createPoint(_point.clonePoint()); + } + + // make lists for edit and undo, and add the changed field FieldEditList editList = new FieldEditList(); FieldEditList undoList = new FieldEditList(); editList.addEdit(new FieldEdit(Field.WAYPT_NAME, _nameField.getText().trim())); @@ -231,4 +238,17 @@ public class PointNameEditor extends GenericFunction || (!prevNull && newNull) || (!prevNull && !newNull && !prevName.equals(newName)); } + + /** + * Check whether a new name has been added + * @return true if it has indeed + */ + private boolean wasNameAdded() + { + String prevName = _point.getWaypointName(); + String newName = _nameField.getText().trim(); + boolean prevNull = (prevName == null || prevName.equals("")); + boolean newNull = (newName == null || newName.equals("")); + return (prevNull && !newNull); + } } diff --git a/src/tim/prune/function/settings/SetEarthdataAuthentication.java b/src/tim/prune/function/settings/SetEarthdataAuthentication.java new file mode 100644 index 0000000..146ed77 --- /dev/null +++ b/src/tim/prune/function/settings/SetEarthdataAuthentication.java @@ -0,0 +1,183 @@ +package tim.prune.function.settings; + +import java.awt.Component; +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Base64; + +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.JTextField; + +import tim.prune.App; +import tim.prune.GenericFunction; +import tim.prune.I18nManager; +import tim.prune.config.Config; +import tim.prune.function.srtm.SrtmGl1Source; +import tim.prune.function.srtm.SrtmSourceException; + +/** + * Set authentication data for the NASA Earthdata systems + */ +public class SetEarthdataAuthentication extends GenericFunction +{ + private JDialog _dialog = null; + private JTextField _usernameField = null; + private JPasswordField _passwordField = null; + private JLabel _authAccepted = null; + + /** + * Constructor + * @param inApp App object + */ + public SetEarthdataAuthentication(App inApp) { + super(inApp); + } + + /** @return name key */ + public String getNameKey() { + return "function.setearthdataauthentication"; + } + + public void begin() + { + if (_dialog == null) + { + _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true); + _dialog.setLocationRelativeTo(_parentFrame); + // Create Gui and show it + _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + _dialog.getContentPane().add(makeDialogComponents()); + _dialog.pack(); + } + prefillCurrentAuth(); + _dialog.setVisible(true); + } + + /** + * Make the dialog components + * @return the GUI components for the dialog + */ + private JPanel makeDialogComponents() + { + // Blurb to explain to the user + JPanel dialogPanel = new JPanel(); + dialogPanel.setLayout(new BorderLayout()); + dialogPanel.add(new JLabel(""+I18nManager.getText("dialog.earthdataauth.intro")+""), BorderLayout.NORTH); + + // username and password fields + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new BorderLayout()); + JPanel usernamePasswordPanel = new JPanel(); + usernamePasswordPanel.setLayout(new GridLayout(2, 2)); + + JLabel usernameLabel = new JLabel(I18nManager.getText("dialog.earthdataauth.user")); + usernamePasswordPanel.add(usernameLabel); + _usernameField = new JTextField(""); + usernamePasswordPanel.add(_usernameField); + + JLabel passwordLabel = new JLabel(I18nManager.getText("dialog.earthdataauth.password")); + usernamePasswordPanel.add(passwordLabel); + _passwordField = new JPasswordField(""); + usernamePasswordPanel.add(_passwordField); + mainPanel.add(usernamePasswordPanel, BorderLayout.CENTER); + + JPanel authStatusPanel = new JPanel(); + authStatusPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); + _authAccepted = new JLabel(" "); + authStatusPanel.add(_authAccepted); + mainPanel.add(authStatusPanel, BorderLayout.SOUTH); + + mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15)); + dialogPanel.add(mainPanel, BorderLayout.CENTER); + + // ok / cancel buttons at bottom + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + JButton checkButton = new JButton(I18nManager.getText("button.check")); + checkButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + testUsernameAndPassword(); + } + }); + buttonPanel.add(checkButton); + JButton okButton = new JButton(I18nManager.getText("button.ok")); + okButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + finish(); + } + }); + 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; + } + + private String getNewAuthString() + { + String username = _usernameField.getText(); + String password = _passwordField.getText(); + return Base64.getEncoder().encodeToString((username + ":" + password).getBytes()); + } + + private void finish() + { + Config.setConfigString(Config.KEY_EARTHDATA_AUTH, getNewAuthString()); + _dialog.dispose(); + } + + private void prefillCurrentAuth() + { + _usernameField.setText(""); + _passwordField.setText(""); + _authAccepted.setText(" "); + + String authString = Config.getConfigString(Config.KEY_EARTHDATA_AUTH); + try + { + String decoded = new String(Base64.getDecoder().decode(authString)); + if (decoded.contains(":")) + { + _usernameField.setText(decoded.split(":", 2)[0]); + _passwordField.setText(decoded.split(":", 2)[1]); + } + } + catch (Exception e) + { + // empty settings, or invalid base64 data --leave blank + return; + } + } + + private void testUsernameAndPassword() + { + String username = _usernameField.getText(); + String password = _passwordField.getText(); + SrtmGl1Source srtmGL1 = new SrtmGl1Source(); + try + { + _authAccepted.setText("..."); + srtmGL1.testAuth(getNewAuthString()); + _authAccepted.setText(I18nManager.getText("dialog.earthdataauth.authaccepted")); + } + catch (SrtmSourceException e) + { + _authAccepted.setText(I18nManager.getText("dialog.earthdataauth.authrejected") + ": " + e.getMessage()); + } + } +} diff --git a/src/tim/prune/function/srtm/DownloadSrtmFunction.java b/src/tim/prune/function/srtm/DownloadSrtmFunction.java index 0350ce3..311ff92 100644 --- a/src/tim/prune/function/srtm/DownloadSrtmFunction.java +++ b/src/tim/prune/function/srtm/DownloadSrtmFunction.java @@ -1,11 +1,5 @@ package tim.prune.function.srtm; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLConnection; import java.util.ArrayList; import javax.swing.JOptionPane; @@ -14,7 +8,6 @@ import tim.prune.App; import tim.prune.GenericFunction; import tim.prune.GpsPrune; import tim.prune.I18nManager; -import tim.prune.config.Config; import tim.prune.data.DoubleRange; import tim.prune.gui.ProgressDialog; @@ -28,19 +21,20 @@ public class DownloadSrtmFunction extends GenericFunction implements Runnable private ProgressDialog _progress = null; /** Flag to check whether this function is currently running or not */ private boolean _running = false; - + private SrtmSource _srtmSource = null; /** * Constructor * @param inApp App object */ - public DownloadSrtmFunction(App inApp) { + public DownloadSrtmFunction(App inApp, SrtmSource inSrtmSource) { super(inApp); + _srtmSource = inSrtmSource; } /** @return name key */ public String getNameKey() { - return "function.downloadsrtm"; + return "function.downloadsrtm."+_srtmSource.getName(); } /** @@ -48,6 +42,17 @@ public class DownloadSrtmFunction extends GenericFunction implements Runnable */ public void begin() { + if (! SrtmDiskCache.ensureCacheIsUsable()) + { + _app.showErrorMessage(getNameKey(), "error.downloadsrtm.nocache"); + return; + } + if (! _srtmSource.isReadyToUse()) + { + _app.showErrorMessage(getNameKey(), getNameKey() + ".needsetup"); + return; + } + _running = true; if (_progress == null) { _progress = new ProgressDialog(_parentFrame, getNameKey()); @@ -61,6 +66,14 @@ public class DownloadSrtmFunction extends GenericFunction implements Runnable * Run method using separate thread */ public void run() + { + ArrayList tileList = buildCoveringTiles(); + downloadTiles(tileList); + // Finished + _running = false; + } + + private ArrayList buildCoveringTiles() { // Compile list of tiles to get ArrayList tileList = new ArrayList(); @@ -89,18 +102,16 @@ public class DownloadSrtmFunction extends GenericFunction implements Runnable } } - downloadTiles(tileList); - // Finished - _running = false; + return tileList; } - /** * Download the tiles of SRTM data * @param inTileList list of tiles to get */ private void downloadTiles(ArrayList inTileList) { + String errorMessage = ""; // Update progress bar if (_progress != null) { @@ -108,62 +119,29 @@ public class DownloadSrtmFunction extends GenericFunction implements Runnable _progress.setValue(0); } - String errorMessage = null; - - // Check the cache is ok - final String diskCachePath = Config.getConfigString(Config.KEY_DISK_CACHE); - if (diskCachePath != null) - { - File srtmDir = new File(diskCachePath, "srtm"); - if (!srtmDir.exists() && !srtmDir.mkdir()) { - // can't create the srtm directory - errorMessage = I18nManager.getText("error.downloadsrtm.nocache"); - } - } - else { - // no cache set up - errorMessage = I18nManager.getText("error.downloadsrtm.nocache"); - } - - // Get urls for each tile - URL[] urls = TileFinder.getUrls(inTileList); int numDownloaded = 0; for (int t=0; t 0) { - _app.showErrorMessage(getNameKey(), "confirm.downloadsrtm.none"); + JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("confirm.downloadsrtm.none")); } } - - /** - * See whether the SRTM file is already available locally - * @param inUrl URL for online resource - * @return file object to write to, or null if already there - */ - private static File getFileToWrite(URL inUrl) - { - 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.canRead() || srtmFile.length() <= 400) { - return srtmFile; - } - } - } - return null; - } - - /** - * @return true if a thread is currently running - */ - public boolean isRunning() - { - return _running; - } } diff --git a/src/tim/prune/function/srtm/LookupSrtmFunction.java b/src/tim/prune/function/srtm/LookupSrtmFunction.java index 1599744..927d2cd 100644 --- a/src/tim/prune/function/srtm/LookupSrtmFunction.java +++ b/src/tim/prune/function/srtm/LookupSrtmFunction.java @@ -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; @@ -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; /** @@ -38,13 +30,9 @@ public class LookupSrtmFunction extends GenericFunction implements Runnable 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; @@ -84,7 +72,10 @@ public class LookupSrtmFunction extends GenericFunction implements Runnable 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()); } @@ -131,8 +122,7 @@ public class LookupSrtmFunction extends GenericFunction implements Runnable 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; @@ -148,12 +138,23 @@ public class LookupSrtmFunction extends GenericFunction implements Runnable 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 @@ -170,94 +171,79 @@ public class LookupSrtmFunction extends GenericFunction implements Runnable _progress.setMaximum(inTileList.size()); _progress.setValue(0); } - String errorMessage = null; - // Get urls for each tile - URL[] urls = TileFinder.getUrls(inTileList); + String errorMessage = ""; for (int t=0; t 0) System.out.println(numVoids + " voids found"); + double altitude = 0.0; + switch (numVoids) { - heights[i] = inStream.read() * 256 + inStream.read(); - if (heights[i] >= 32768) {heights[i] -= 65536;} + 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; } - } - // else { - // System.out.println("length not ok: " + entry.getSize()); - // } - // Close stream from url - inStream.close(); - } - - if (entryOk) - { - // 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)) + // Special case for terrain tracks, don't interpolate voids yet + if (!_normalTrack && numVoids > 0) { + altitude = VOID_VAL; + } + if (altitude != VOID_VAL) { - if (new SrtmTile(point).equals(tile)) - { - double x = (point.getLongitude().getDouble() - tile.getLongitude()) * 1200; - double y = 1201 - (point.getLatitude().getDouble() - tile.getLatitude()) * 1200; - int idx1 = ((int)y)*1201 + (int)x; - try - { - int[] fouralts = {heights[idx1], heights[idx1+1], heights[idx1-1201], heights[idx1-1200]}; - 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) { - // System.err.println("lat=" + point.getLatitude().getDouble() + ", x=" + x + ", y=" + y + ", idx=" + idx1); - } - } + 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"; + } } } - catch (IOException ioe) {errorMessage = ioe.getClass().getName() + " - " + ioe.getMessage(); - } } } @@ -266,6 +252,10 @@ public class LookupSrtmFunction extends GenericFunction implements Runnable return; } + if (! errorMessage.equals("")) { + _app.showErrorMessageNoLookup(getNameKey(), errorMessage); + return; + } if (numAltitudesFound > 0) { // Inform app including undo information @@ -278,9 +268,6 @@ public class LookupSrtmFunction extends GenericFunction implements Runnable I18nManager.getTextWithNumber("confirm.lookupsrtm", numAltitudesFound)); } } - else if (errorMessage != null) { - _app.showErrorMessageNoLookup(getNameKey(), errorMessage); - } else if (inTileList.size() > 0) { _app.showErrorMessage(getNameKey(), "error.lookupsrtm.nonefound"); } @@ -289,36 +276,6 @@ public class LookupSrtmFunction extends GenericFunction implements Runnable } } - /** - * 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()); - } - /** * Perform a bilinear interpolation on the given altitude array * @param inAltitudes array of four altitude values on corners of square (bl, br, tl, tr) diff --git a/src/tim/prune/function/srtm/Srtm3Source.java b/src/tim/prune/function/srtm/Srtm3Source.java new file mode 100644 index 0000000..14fad59 --- /dev/null +++ b/src/tim/prune/function/srtm/Srtm3Source.java @@ -0,0 +1,164 @@ +package tim.prune.function.srtm; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.HttpURLConnection; + +import tim.prune.GpsPrune; +import tim.prune.I18nManager; + +public class Srtm3Source extends SrtmSource { + /** URL prefix for all tiles */ + private static final String URL_PREFIX = "https://dds.cr.usgs.gov/srtm/version2_1/SRTM3/"; + /** Directory names for each continent */ + private static final String[] CONTINENTS = {"", "Eurasia", "North_America", "Australia", + "Islands", "South_America", "Africa"}; + private byte[] _continents_lookup; + + + public Srtm3Source() + { + _continents_lookup = populateContinents(); + } + + public String getNameKey() + { + return "function.downloadsrtm." + getName(); + } + + public String getName() + { + return "SRTM3_v21"; + } + + protected String getSourceExtension() + { + return ".hgt.zip"; + } + + /** + * Read the dat file and get the contents + * @return byte array containing file contents + */ + private static byte[] populateContinents() + { + InputStream in = null; + try + { + // Need absolute path to dat file + in = Srtm3Source.class.getResourceAsStream("/tim/prune/function/srtm/srtmtiles.dat"); + if (in != null) + { + byte[] buffer = new byte[in.available()]; + in.read(buffer); + in.close(); + return buffer; + } + } + 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; + } + + /** + * Get the Url for the given tile + * @param inTile Tile to get + * @return URL + */ + private URL buildUrl(SrtmTile inTile) + throws SrtmSourceException + { + + // Get byte from lookup array + int idx = (inTile.getLatitude() + 59)*360 + (inTile.getLongitude() + 180); + int dir; + try + { + dir = _continents_lookup[idx]; + } + catch (ArrayIndexOutOfBoundsException e) + { + throw new SrtmSourceException("Could not find continent for tile "+inTile.getTileName()); + } + try + { + return new URL(URL_PREFIX + CONTINENTS[dir] + "/" + inTile.getTileName() + getSourceExtension()); + } + catch (MalformedURLException e) + { + throw new SrtmSourceException("Could not build URL for tile "+inTile.getTileName()); + } + } + + public boolean isReadyToUse() + { + return true; + } + + public boolean downloadTile(SrtmTile inTile) + throws SrtmSourceException + { + int redirects = 5; + URL tileUrl = buildUrl(inTile); + File outputFile = getCacheFileName(inTile); + System.out.println("Download: Need to download: " + tileUrl); + + try + { + HttpURLConnection conn = (HttpURLConnection) tileUrl.openConnection(); + + // Define streams + FileOutputStream outStream = null; + InputStream inStream = null; + + conn.setRequestProperty("User-Agent", "GpsPrune v" + GpsPrune.VERSION_NUMBER); + + int status = conn.getResponseCode(); + if (status == 200) + { + inStream = conn.getInputStream(); + } + else if (status == 404) + { + throw new SrtmSourceException("Tile not found: "+conn.getURL()); + } + else + { + throw new SrtmSourceException("Invalid response from server: " +status+conn.getContent()); + } + + outStream = new FileOutputStream(outputFile); + + int c; + while ((c = inStream.read()) != -1) + { + outStream.write(c); + } + // Make sure streams are closed + try {inStream.close();} catch (Exception e) {} + try {outStream.close();} catch (Exception e) {} + return true; + } + catch (IOException e) + { + throw new SrtmSourceException("Error while downloading tile "+inTile.getTileName()+": "+e.getMessage()); + } + } + + public int getRowSize(SrtmTile inTile) + { + return 1201; + } +} diff --git a/src/tim/prune/function/srtm/SrtmDiskCache.java b/src/tim/prune/function/srtm/SrtmDiskCache.java new file mode 100644 index 0000000..80159e0 --- /dev/null +++ b/src/tim/prune/function/srtm/SrtmDiskCache.java @@ -0,0 +1,49 @@ +package tim.prune.function.srtm; + +import java.io.File; + +import tim.prune.config.Config; +import tim.prune.I18nManager; + +public class SrtmDiskCache { + + private static boolean _cacheIsUsable = false; + private static File _cacheDir = null; + + public static boolean ensureCacheIsUsable() + { + + if (_cacheIsUsable) + { + return true; + } + // Check the cache is ok + String diskCachePath = Config.getConfigString(Config.KEY_DISK_CACHE); + if (diskCachePath == null) + { + return false; + } + File srtmDir = new File(diskCachePath, "srtm"); + if (!srtmDir.exists() && !srtmDir.mkdir()) { + // can't create the srtm directory + return false; + } + _cacheIsUsable = true; + _cacheDir = srtmDir; + return true; + } + + public static File getCacheDir(String inSourceName) + { + if (_cacheDir == null) + { + ensureCacheIsUsable(); + } + File cacheDir = new File(_cacheDir, inSourceName); + if (!cacheDir.exists() && !cacheDir.mkdir()) { + // can't create the srtm directory + return null; + } + return cacheDir; + } +} diff --git a/src/tim/prune/function/srtm/SrtmGl1Source.java b/src/tim/prune/function/srtm/SrtmGl1Source.java new file mode 100644 index 0000000..51e6d77 --- /dev/null +++ b/src/tim/prune/function/srtm/SrtmGl1Source.java @@ -0,0 +1,176 @@ +package tim.prune.function.srtm; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.HttpURLConnection; + +import tim.prune.App; +import tim.prune.GpsPrune; +import tim.prune.config.Config; + +/** + * Create an account at: https://urs.earthdata.nasa.gov/users/new + * Data policy: https://lpdaac.usgs.gov/data/data-citation-and-policies/ + * + */ + +public class SrtmGl1Source extends SrtmSource { + /** URL prefix for all tiles */ + private static final String URL_PREFIX = "https://e4ftl01.cr.usgs.gov/MEASURES/SRTMGL1.003/2000.02.11/"; + /** Auth URL */ + private static final String AUTH_URL = "urs.earthdata.nasa.gov"; + + + public SrtmGl1Source() + { + CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL)); + } + + public String getName() + { + return "SRTMGL1_v003"; + } + + protected String getSourceExtension() + { + return ".SRTMGL1.hgt.zip"; + } + + private URL buildUrl(SrtmTile inTile) + throws SrtmSourceException + { + try { + return new URL(URL_PREFIX + inTile.getTileName() + getSourceExtension()); + } + catch (MalformedURLException e) + { + throw new SrtmSourceException(e.getMessage()); + } + } + + public boolean isReadyToUse() + { + return getAuth() != null; + } + + private String getAuth() + { + String authString = Config.getConfigString(Config.KEY_EARTHDATA_AUTH); + if (authString != null) + { + return "Basic " + authString; + } + else + { + return null; + } + } + + public boolean downloadTile(SrtmTile inTile) + throws SrtmSourceException + { + return downloadTile(inTile, getAuth()); + } + + private boolean downloadTile(SrtmTile inTile, String auth) + throws SrtmSourceException + { + URL tileUrl = buildUrl(inTile); + File outputFile = getCacheFileName(inTile); + System.out.println("Download: Need to download: " + tileUrl); + try + { + HttpURLConnection conn = (HttpURLConnection) tileUrl.openConnection(); + long fileLength = 0L; + + // Define streams + FileOutputStream outStream = null; + InputStream inStream = null; + + // Documentation about HTTP interface at: + // https://wiki.earthdata.nasa.gov/display/EL/How+To+Access+Data+With+Java + int redirects = 0; + + while (redirects < 10) { + redirects++; + + conn.setRequestProperty("User-Agent", "GpsPrune v" + GpsPrune.VERSION_NUMBER); + conn.setInstanceFollowRedirects(false); + conn.setUseCaches(false); + if (conn.getURL().getHost().equals(AUTH_URL)) + { + conn.setRequestProperty("Authorization", auth); + } + + int status = conn.getResponseCode(); + if (status == 200) + { + // Found the tile, we're good + inStream = conn.getInputStream(); + fileLength = conn.getContentLengthLong(); + break; + } + else if (status == 302) + { + // redirected to SSO server then back to original resource + String newUrl = conn.getHeaderField("Location"); + conn = (HttpURLConnection) (new URL(newUrl)).openConnection(); + } + else if (status == 404) + { + throw new SrtmSourceException("Tile " + inTile.getTileName() + " not found at " + conn.getURL()); + } + else + { + throw new SrtmSourceException("Invalid response from server: " + status + conn.getResponseMessage()); + } + } + + // _progress.setValue(t * 10 + 1); + outStream = new FileOutputStream(outputFile); + + // Copy all the bytes to the file + int c; + long written = 0L; + while ((c = inStream.read()) != -1) + { + outStream.write(c); + written++; + // _progress.setValue(t * 10 + 1 + (int) ((10 * written) / fileLength)); + } + // Make sure streams are closed + try {inStream.close();} catch (Exception e) {} + try {outStream.close();} catch (Exception e) {} + return true; + } + catch (IOException e) + { + throw new SrtmSourceException("Error while downloading tile " + inTile.getTileName() + ": "+e.getMessage()); + } + } + + public boolean testAuth(String auth) + throws SrtmSourceException + { + // The only thing special about this tile is that it's the smallest tile + // It covers small islands in Malaysia + SrtmTile testTile = new SrtmTile(7, 117); + if (isCached(testTile)) + { + getCacheFileName(testTile).delete(); + } + return downloadTile(testTile, auth); + } + + public int getRowSize(SrtmTile inTile) + { + return 3601; + } +} diff --git a/src/tim/prune/function/srtm/SrtmSource.java b/src/tim/prune/function/srtm/SrtmSource.java new file mode 100644 index 0000000..5651a3f --- /dev/null +++ b/src/tim/prune/function/srtm/SrtmSource.java @@ -0,0 +1,69 @@ +package tim.prune.function.srtm; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public abstract class SrtmSource { + public abstract String getName(); + public abstract boolean isReadyToUse(); + public abstract boolean downloadTile(SrtmTile inTile) + throws SrtmSourceException; + public abstract int getRowSize(SrtmTile inTile); + protected abstract String getSourceExtension(); + + public int[] getTileHeights(SrtmTile inTile) + throws SrtmSourceException + { + File cacheFileName = getCacheFileName(inTile); + if (cacheFileName == null) + { + throw new SrtmSourceException("Tile "+inTile.getTileName()+" not in cache"); + } + try + { + ZipInputStream inStream = new ZipInputStream(new FileInputStream(cacheFileName)); + ZipEntry entry = inStream.getNextEntry(); + int rowSize = getRowSize(inTile); + int tileSize = rowSize * rowSize; + if (entry.getSize() != 2 * tileSize) + { + throw new SrtmSourceException("Tile file "+cacheFileName+" does not have the expected size"); + } + int[] heights = new int[tileSize]; + // Read entire file contents into one byte array + for (int i = 0; i < heights.length; i++) + { + heights[i] = inStream.read() * 256 + inStream.read(); + if (heights[i] >= 32768) {heights[i] -= 65536;} + } + // Close stream + inStream.close(); + return heights; + } + catch (IOException e) + { + throw new SrtmSourceException("Failure opening "+cacheFileName+" for reading:"+e.getMessage()); + } + + } + + protected File getCacheDir() + { + return SrtmDiskCache.getCacheDir(getName()); + } + + protected File getCacheFileName(SrtmTile inTile) + { + String fileName = inTile.getTileName() + getSourceExtension(); + return new File(getCacheDir(), fileName); + } + + public boolean isCached(SrtmTile inTile) + { + return getCacheFileName(inTile).exists(); + } +} diff --git a/src/tim/prune/function/srtm/SrtmSourceException.java b/src/tim/prune/function/srtm/SrtmSourceException.java new file mode 100644 index 0000000..37b2db5 --- /dev/null +++ b/src/tim/prune/function/srtm/SrtmSourceException.java @@ -0,0 +1,7 @@ +package tim.prune.function.srtm; + +public class SrtmSourceException extends Exception { + public SrtmSourceException(String message) { + super(message); + } +} diff --git a/src/tim/prune/function/srtm/SrtmTile.java b/src/tim/prune/function/srtm/SrtmTile.java index 301bbaf..7bc1d38 100644 --- a/src/tim/prune/function/srtm/SrtmTile.java +++ b/src/tim/prune/function/srtm/SrtmTile.java @@ -67,7 +67,20 @@ public class SrtmTile + (_longitude >= 0?"E":"W") + (Math.abs(_longitude) < 100?"0":"") + (Math.abs(_longitude) < 10?"0":"") - + Math.abs(_longitude) - + ".hgt.zip"; + + Math.abs(_longitude); + } + + public SrtmSource findBestCachedSource() + { + SrtmSource[] sources = {new SrtmGl1Source(), + new Srtm3Source() }; + for (int i = 0; i < sources.length; i++) + { + if (sources[i].isCached(this)) + { + return sources[i]; + } + } + return null; } } diff --git a/src/tim/prune/function/srtm/TileFinder.java b/src/tim/prune/function/srtm/TileFinder.java deleted file mode 100644 index 60a9479..0000000 --- a/src/tim/prune/function/srtm/TileFinder.java +++ /dev/null @@ -1,82 +0,0 @@ -package tim.prune.function.srtm; - -import java.io.InputStream; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; - - -/** - * Class to get the URLs of the SRTM tiles - * using the srtmtiles.dat file - */ -public abstract class TileFinder -{ - /** URL prefix for all tiles */ - private static final String URL_PREFIX = "https://dds.cr.usgs.gov/srtm/version2_1/SRTM3/"; - /** Directory names for each continent */ - private static final String[] CONTINENTS = {"", "Eurasia", "North_America", "Australia", - "Islands", "South_America", "Africa"}; - - - /** - * Get the Urls for the given list of tiles - * @param inTiles list of Tiles to get - * @return array of URLs - */ - public static URL[] getUrls(ArrayList inTiles) - { - if (inTiles == null || inTiles.size() < 1) {return null;} - URL[] urls = new URL[inTiles.size()]; - // Read dat file into array - byte[] lookup = readDatFile(); - for (int t=0; t 0) { - try { - urls[t] = new URL(URL_PREFIX + CONTINENTS[dir] + "/" + tile.getTileName()); - } catch (MalformedURLException e) {} // ignore error, url stays null - } - } catch (ArrayIndexOutOfBoundsException e) {} // ignore error, url stays null - } - return urls; - } - - /** - * Read the dat file and get the contents - * @return byte array containing file contents - */ - private static byte[] readDatFile() - { - InputStream in = null; - try - { - // Need absolute path to dat file - in = TileFinder.class.getResourceAsStream("/tim/prune/function/srtm/srtmtiles.dat"); - if (in != null) - { - byte[] buffer = new byte[in.available()]; - in.read(buffer); - in.close(); - return buffer; - } - } - 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/src/tim/prune/gui/DisplayUtils.java b/src/tim/prune/gui/DisplayUtils.java index 25640d8..2300296 100644 --- a/src/tim/prune/gui/DisplayUtils.java +++ b/src/tim/prune/gui/DisplayUtils.java @@ -36,7 +36,8 @@ public abstract class DisplayUtils if (inNumSecs < 86400L) return "" + (inNumSecs / 60 / 60) + I18nManager.getText("display.range.time.hours") + " " + ((inNumSecs / 60) % 60) + I18nManager.getText("display.range.time.mins"); if (inNumSecs < 432000L) return "" + (inNumSecs / 86400L) + I18nManager.getText("display.range.time.days") - + " " + (inNumSecs / 60 / 60) % 24 + I18nManager.getText("display.range.time.hours"); + + " " + (inNumSecs / 60 / 60) % 24 + I18nManager.getText("display.range.time.hours") + + " " + ((inNumSecs / 60) % 60) + I18nManager.getText("display.range.time.mins"); if (inNumSecs < 86400000L) return "" + (inNumSecs / 86400L) + I18nManager.getText("display.range.time.days"); return "big"; } diff --git a/src/tim/prune/gui/MenuManager.java b/src/tim/prune/gui/MenuManager.java index bce3b4d..bd0d378 100644 --- a/src/tim/prune/gui/MenuManager.java +++ b/src/tim/prune/gui/MenuManager.java @@ -34,6 +34,9 @@ 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; /** * Class to manage the menu bar and tool bar, @@ -77,6 +80,7 @@ public class MenuManager implements DataSubscriber 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; @@ -93,7 +97,7 @@ public class MenuManager implements DataSubscriber 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; @@ -257,8 +261,15 @@ public class MenuManager implements DataSubscriber // 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 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); @@ -425,6 +436,8 @@ public class MenuManager implements DataSubscriber 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) { @@ -651,6 +664,8 @@ public class MenuManager implements DataSubscriber 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)); @@ -889,7 +904,7 @@ public class MenuManager implements DataSubscriber _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)); @@ -956,6 +971,7 @@ public class MenuManager implements DataSubscriber _reverseItem.setEnabled(hasRange); _addTimeOffsetItem.setEnabled(hasRange); _addAltitudeOffsetItem.setEnabled(hasRange); + _removeAltitudesItem.setEnabled(hasRange); _convertNamesToTimesItem.setEnabled(hasRange && _track.hasWaypoints()); _deleteFieldValuesItem.setEnabled(hasRange); _fullRangeDetailsItem.setEnabled(hasRange); diff --git a/src/tim/prune/gui/SelectorDisplay.java b/src/tim/prune/gui/SelectorDisplay.java index fa6630d..9dfc24f 100644 --- a/src/tim/prune/gui/SelectorDisplay.java +++ b/src/tim/prune/gui/SelectorDisplay.java @@ -228,8 +228,9 @@ public class SelectorDisplay extends GenericDisplay else if (numFiles > 1) { final String labelText = I18nManager.getText("details.track.numfiles") + ": " + numFiles; + final String filenameString = String.join(", ", _trackInfo.getFileInfo().getFilenames()); _filenameLabel.setText(labelText); - _filenameLabel.setToolTipText(labelText); + _filenameLabel.setToolTipText(filenameString); } else { diff --git a/src/tim/prune/gui/Viewport.java b/src/tim/prune/gui/Viewport.java index 25eba2f..aec78a6 100644 --- a/src/tim/prune/gui/Viewport.java +++ b/src/tim/prune/gui/Viewport.java @@ -40,4 +40,12 @@ public class Viewport 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(); + } } diff --git a/src/tim/prune/gui/map/MapCanvas.java b/src/tim/prune/gui/map/MapCanvas.java index aa25dbc..ad38d81 100644 --- a/src/tim/prune/gui/map/MapCanvas.java +++ b/src/tim/prune/gui/map/MapCanvas.java @@ -92,7 +92,7 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe 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 */ @@ -259,8 +259,8 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe // 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); @@ -337,16 +337,19 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe /** * 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); } @@ -360,70 +363,59 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe 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); @@ -1074,66 +1066,63 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe */ 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; @@ -1408,10 +1397,6 @@ public class MapCanvas extends JPanel implements MouseListener, MouseMotionListe } } repaint(); - // enable or disable components - boolean hasData = _track.getNumPoints() > 0; - _topPanel.setVisible(hasData); - _sidePanel.setVisible(hasData); // grab focus for the key presses this.requestFocus(); } diff --git a/src/tim/prune/gui/map/MapPosition.java b/src/tim/prune/gui/map/MapPosition.java index 25f05fc..71ab6cd 100644 --- a/src/tim/prune/gui/map/MapPosition.java +++ b/src/tim/prune/gui/map/MapPosition.java @@ -30,14 +30,14 @@ public class MapPosition * @param inWidth width of display * @param inHeight height of display */ - public void zoomToXY(double inMinX, double inMaxX, double inMinY, double inMaxY, int inWidth, int inHeight) + public void zoomToXY(double inMinX, double inMaxX, double inMinY, double inMaxY, int inWidth, int inHeight, int maxZoom) { // System.out.println("Zooming to " + inMinX + ", " + inMaxX + ", " + inMinY + ", " + inMaxY + "; width=" + inWidth + ", height=" + inHeight); double diffX = Math.abs(inMaxX - inMinX); double diffY = Math.abs(inMaxY - inMinY); // Find out what zoom level to go to int requiredZoom = -1; - for (int currZoom = MAX_ZOOM; currZoom >= 2; currZoom--) + for (int currZoom = maxZoom; currZoom >= 2; currZoom--) { if (transformToPixels(diffX, currZoom) < inWidth && transformToPixels(diffY, currZoom) < inHeight) diff --git a/src/tim/prune/gui/map/MapSource.java b/src/tim/prune/gui/map/MapSource.java index c4e2946..177ad67 100644 --- a/src/tim/prune/gui/map/MapSource.java +++ b/src/tim/prune/gui/map/MapSource.java @@ -104,7 +104,7 @@ public abstract class MapSource urlstr = "http://" + urlstr; } // check trailing / - if (!urlstr.endsWith("/")) { + if (!urlstr.endsWith("/") && !urlstr.contains("?")) { urlstr = urlstr + "/"; } // Validate current url, return null if not ok diff --git a/src/tim/prune/gui/map/MapTileManager.java b/src/tim/prune/gui/map/MapTileManager.java index f7370c5..d1e0047 100644 --- a/src/tim/prune/gui/map/MapTileManager.java +++ b/src/tim/prune/gui/map/MapTileManager.java @@ -70,10 +70,19 @@ public class MapTileManager implements ImageObserver * @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; + } /** diff --git a/src/tim/prune/gui/map/OsmMapSource.java b/src/tim/prune/gui/map/OsmMapSource.java index 925fcf6..e3fe589 100644 --- a/src/tim/prune/gui/map/OsmMapSource.java +++ b/src/tim/prune/gui/map/OsmMapSource.java @@ -152,12 +152,31 @@ public class OsmMapSource extends MapSource /** * Make the URL to get the specified tile + * @param inLayerNum layer number + * @param inZoom zoom level + * @param inX x coordinate + * @param inY y coordinate + * @return relative file path as String */ public String makeURL(int inLayerNum, int inZoom, int inX, int inY) { // Check if the base url has a [1234], if so replace at random - StringBuffer url = new StringBuffer(); - url.append(pickServerUrl(_baseUrls[inLayerNum])); + String baseUrl = pickServerUrl(_baseUrls[inLayerNum]); + return makeUrl(baseUrl, inLayerNum, inZoom, inX, inY); + } + + public String makeUrl(String baseUrl, int inLayerNum, int inZoom, int inX, int inY) + { + // If the base URL has {x}/{y} placeholders, use them + if (baseUrl.contains("{x}")) { + baseUrl = baseUrl.replace("{z}", Integer.toString(inZoom)) + .replace("{x}", Integer.toString(inX)) + .replace("{y}", Integer.toString(inY)); + return baseUrl; + } + + // Else simply append the tile indices and file extension + StringBuffer url = new StringBuffer(baseUrl); url.append(inZoom).append('/').append(inX).append('/').append(inY); url.append('.').append(getFileExtension(inLayerNum)); if (_apiKey != null) @@ -167,6 +186,26 @@ public class OsmMapSource extends MapSource return url.toString(); } + /** + * Make a relative file path from the base directory including site name + * @param inLayerNum layer number + * @param inZoom zoom level + * @param inX x coordinate + * @param inY y coordinate + * @return relative file path as String + */ + public String makeFilePath(int inLayerNum, int inZoom, int inX, int inY) + { + String siteName = getSiteName(inLayerNum); + String filePath = makeUrl(siteName, inLayerNum, inZoom, inX, inY); + int indexParam = filePath.indexOf("?"); + if (indexParam > 0) + { + filePath = filePath.substring(0, indexParam); + } + return filePath; + } + /** * @return maximum zoom level */ diff --git a/src/tim/prune/lang/prune-texts_en.properties b/src/tim/prune/lang/prune-texts_en.properties index aa440a3..fd50356 100644 --- a/src/tim/prune/lang/prune-texts_en.properties +++ b/src/tim/prune/lang/prune-texts_en.properties @@ -94,6 +94,7 @@ function.interpolate=Interpolate points 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 @@ -114,6 +115,9 @@ 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=Download 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=Download SRTM 3 arc-second tiles function.getwikipedia=Get nearby Wikipedia articles function.searchwikipedianames=Search Wikipedia by name function.searchosmpois=Get nearby OSM points @@ -150,6 +154,7 @@ function.managetilecache=Manage tile cache 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 @@ -576,6 +581,11 @@ dialog.displaysettings.size.small=Small dialog.displaysettings.size.medium=Medium dialog.displaysettings.size.large=Large dialog.downloadosm.desc=Confirm to download the raw OSM data for the specified area: +dialog.earthdataauth.intro=

Configure username and password to access your NASA Earthdata login account.

Create an account at https://urs.earthdata.nasa.gov/users/new.

+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 @@ -633,6 +643,7 @@ confirm.mergetracksegments=Track segments merged 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 @@ -853,6 +864,7 @@ undo.splitsegments=split track segments 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 diff --git a/src/tim/prune/undo/UndoRemoveAltitudes.java b/src/tim/prune/undo/UndoRemoveAltitudes.java new file mode 100644 index 0000000..becf2d7 --- /dev/null +++ b/src/tim/prune/undo/UndoRemoveAltitudes.java @@ -0,0 +1,65 @@ +package tim.prune.undo; + +import tim.prune.I18nManager; +import tim.prune.UpdateMessageBroker; +import tim.prune.data.Altitude; +import tim.prune.data.DataPoint; +import tim.prune.data.TrackInfo; + +/** + * Undo removing (ie: restore the original) altitude from points + */ +public class UndoRemoveAltitudes implements UndoOperation +{ + /** Start index of section */ + private int _startIndex; + /** altitude values before operation */ + private Altitude[] _altitudes; + + + /** + * Constructor + * @param inTrackInfo track info object + */ + public UndoRemoveAltitudes(TrackInfo inTrackInfo, int inStart, int inEnd) + { + _startIndex = inStart; + final int numPoints = inEnd - inStart + 1; + // Make array of cloned altitude objects + _altitudes = new Altitude[numPoints]; + for (int i=0; i