diff --git a/resources/images/preferences/elevation.svg b/resources/images/preferences/elevation.svg
new file mode 100644
index 000000000..cab04bbc6
--- /dev/null
+++ b/resources/images/preferences/elevation.svg
@@ -0,0 +1,89 @@
+
+
diff --git a/resources/images/statusline/ele.svg b/resources/images/statusline/ele.svg
new file mode 100644
index 000000000..4a8ac7392
--- /dev/null
+++ b/resources/images/statusline/ele.svg
@@ -0,0 +1,58 @@
+
+
diff --git a/src/org/openstreetmap/josm/data/elevation/ElevationHelper.java b/src/org/openstreetmap/josm/data/elevation/ElevationHelper.java
new file mode 100644
index 000000000..a0cfe7d59
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/elevation/ElevationHelper.java
@@ -0,0 +1,283 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.elevation;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.Optional;
+
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.SystemOfMeasurement;
+import org.openstreetmap.josm.data.coor.ILatLon;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.elevation.gpx.GeoidCorrectionKind;
+import org.openstreetmap.josm.data.gpx.WayPoint;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Provides methods to access way point attributes and some utility methods regarding elevation stuff (
+ * e. g. special text formats, unit conversion, geoid calc).
+ * @author Oliver Wieland <oliver.wieland@online.de>
+ */
+public final class ElevationHelper {
+
+ private ElevationHelper() {
+ // Hide default constructor for utilities classes
+ }
+
+ /**
+ * The 'no elevation' data magic.
+ * @see ElevationHelper#isValidElevation
+ */
+ public static final double NO_ELEVATION = Double.NaN;
+
+ /**
+ * The name of the elevation height of a way point.
+ */
+ public static final String HEIGHT_ATTRIBUTE = "ele";
+
+ private static GeoidCorrectionKind geoidKind = GeoidCorrectionKind.None;
+
+ /**
+ * Gets the current mode of GEOID correction.
+ */
+ public static GeoidCorrectionKind getGeoidKind() {
+ return geoidKind;
+ }
+
+ public static void setGeoidKind(GeoidCorrectionKind geoidKind) {
+ ElevationHelper.geoidKind = geoidKind;
+ }
+
+ /**
+ * Checks if given value is a valid elevation value.
+ *
+ * @param ele the ele
+ * @return true, if is valid elevation
+ * @see ElevationHelper#NO_ELEVATION
+ */
+ public static boolean isValidElevation(double ele) {
+ return !Double.isNaN(ele);
+ }
+
+ /**
+ * Gets the elevation (Z coordinate) of a GPX way point in meter or feet (for
+ * US, UK, ZA, AU, NZ and CA).
+ *
+ * @param wpt
+ * The way point instance.
+ * @return The x coordinate or NO_ELEVATION, if the given way point is null or contains
+ * not height attribute.
+ */
+ public static double getElevation(WayPoint wpt) {
+ if (wpt == null) return NO_ELEVATION;
+
+ // try to get elevation from HGT file
+ double eleInt = getSrtmElevation(wpt.getCoor());
+ if (isValidElevation(eleInt)) {
+ return eleInt;
+ }
+
+ // no HGT, check for elevation data in GPX
+ if (!wpt.attr.containsKey(HEIGHT_ATTRIBUTE)) {
+ // GPX has no elevation data :-(
+ return NO_ELEVATION;
+ }
+
+ // Parse elevation from GPX data
+ String height = wpt.getString(ElevationHelper.HEIGHT_ATTRIBUTE);
+ try {
+ return Double.parseDouble(height);
+ } catch (NumberFormatException e) {
+ Logging.error(String.format("Cannot parse double from '%s': %s", height, e.getMessage()));
+ return NO_ELEVATION;
+ }
+ }
+
+ private static double getElevation(LatLon ll) {
+ return getSrtmElevation(ll);
+ }
+
+ /**
+ * Computes the slope in percent between two way points. E. g. an elevation gain of 12m
+ * within a distance of 100m is equal to a slope of 12%.
+ *
+ * @param w1 the first way point
+ * @param w2 the second way point
+ * @return the slope in percent
+ */
+ public static double computeSlope(LatLon w1, LatLon w2) {
+ // same coordinates? -> return 0, if yes
+ if (w1.equals(w2)) return 0;
+
+ // get distance in meters and divide it by 100 in advance
+ double distInMeter = w1.greatCircleDistance((ILatLon) w2) / 100.0;
+
+ // get elevation (difference)
+ int ele1 = (int) ElevationHelper.getElevation(w1);
+ int ele2 = (int) ElevationHelper.getElevation(w2);
+ int dH = ele2 - ele1;
+
+ // Slope in percent is define as elevation gain/loss in meters related to a distance of 100m
+ return dH / distInMeter;
+ }
+
+ /**
+ * Gets the elevation string for a given elevation, e. g "300m" or "800ft".
+ */
+ public static String getElevationText(int elevation) {
+ return SystemOfMeasurement.getSystemOfMeasurement().getDistText(elevation);
+ }
+
+ /**
+ * Gets the elevation string for a given elevation, e. g "300m" or "800ft".
+ */
+ public static String getElevationText(double elevation) {
+ return SystemOfMeasurement.getSystemOfMeasurement().getDistText((int) Math.round(elevation));
+ }
+
+ /**
+ * Gets the elevation string for a given way point, e. g "300m" or "800ft".
+ *
+ * @param wpt the way point
+ * @return the elevation text
+ */
+ public static String getElevationText(WayPoint wpt) {
+ if (wpt == null) return "-";
+
+ return getElevationText(ElevationHelper.getElevation(wpt));
+ }
+
+ /**
+ * Get the time string for a given way point.
+ */
+ public static String getTimeText(WayPoint wpt) {
+ if (wpt == null) return null;
+
+ int hour = ElevationHelper.getHourOfWayPoint(wpt);
+ int min = ElevationHelper.getMinuteOfWayPoint(wpt);
+ return String.format("%02d:%02d", hour, min);
+ }
+
+ /**
+ * Gets the SRTM elevation (Z coordinate) of the given coordinate.
+ *
+ * @param ll
+ * The coordinate.
+ * @return The z coordinate or {@link Double#NaN}, if elevation value could not be obtained
+ * not height attribute.
+ */
+ public static double getSrtmElevation(ILatLon ll) {
+ if (ll != null) {
+ // Try to read data from SRTM file
+ // TODO: Option to switch this off
+ double eleHgt = HgtReader.getInstance().getElevationFromHgt(ll);
+
+ if (isValidElevation(eleHgt)) {
+ return eleHgt;
+ }
+ }
+ return NO_ELEVATION;
+ }
+
+ /**
+ * Get the bounds for the pixel elevation for the latitude
+ * @param location The location to get
+ * @return The bounds for the elevation area
+ */
+ public static Optional getBounds(ILatLon location) {
+ if (location != null) {
+ return HgtReader.getBounds(location);
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Checks given area for SRTM data.
+ *
+ * @param bounds the bounds/area to check
+ * @return true, if SRTM data are present; otherwise false
+ */
+ public static boolean hasSrtmData(Bounds bounds) {
+ if (bounds == null) return false;
+
+ LatLon tl = bounds.getMin();
+ LatLon br = bounds.getMax();
+
+ return isValidElevation(getSrtmElevation(tl)) &&
+ isValidElevation(getSrtmElevation(br));
+ }
+
+ /*
+ * Gets the geoid height for the given way point. See also {@link
+ * GeoidData}.
+ */
+ public static byte getGeoidCorrection(WayPoint wpt) {
+ /*
+ int lat = (int)Math.round(wpt.getCoor().lat());
+ int lon = (int)Math.round(wpt.getCoor().lon());
+ byte geoid = GeoidData.getGeoid(lat, lon);
+
+ System.out.println(
+ String.format("Geoid(%d, %d) = %d", lat, lon, geoid));
+ */
+ return 0;
+ }
+
+ /**
+ * Reduces a given list of way points to the specified target size.
+ *
+ * @param origList
+ * The original list containing the way points.
+ * @param targetSize
+ * The desired target size of the list. The resulting list may
+ * contain fewer items, so targetSize should be considered as
+ * maximum.
+ * @return A list containing the reduced list.
+ */
+ public static List downsampleWayPoints(List origList,
+ int targetSize) {
+ if (origList == null)
+ return null;
+ if (targetSize <= 0)
+ throw new IllegalArgumentException(
+ "targetSize must be greater than zero");
+
+ int origSize = origList.size();
+ if (origSize <= targetSize) {
+ return origList;
+ }
+
+ int delta = (int) Math.max(Math.ceil(origSize / targetSize), 2);
+
+ List res = new ArrayList<>(targetSize);
+ for (int i = 0; i < origSize; i += delta) {
+ res.add(origList.get(i));
+ }
+
+ return res;
+ }
+
+ /**
+ * Gets the hour value of a way point in 24h format.
+ */
+ public static int getHourOfWayPoint(WayPoint wpt) {
+ if (wpt == null) return -1;
+
+ Calendar calendar = GregorianCalendar.getInstance(); // creates a new calendar instance
+ calendar.setTimeInMillis(wpt.getTimeInMillis()); // assigns calendar to given date
+ return calendar.get(Calendar.HOUR_OF_DAY);
+ }
+
+ /**
+ * Gets the minute value of a way point in 24h format.
+ */
+ public static int getMinuteOfWayPoint(WayPoint wpt) {
+ if (wpt == null) return -1;
+
+ Calendar calendar = GregorianCalendar.getInstance(); // creates a new calendar instance
+ calendar.setTimeInMillis(wpt.getTimeInMillis()); // assigns calendar to given date
+ return calendar.get(Calendar.MINUTE);
+ }
+}
diff --git a/src/org/openstreetmap/josm/data/elevation/HgtDownloadListener.java b/src/org/openstreetmap/josm/data/elevation/HgtDownloadListener.java
new file mode 100644
index 000000000..c2de17a6c
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/elevation/HgtDownloadListener.java
@@ -0,0 +1,46 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.elevation;
+
+import java.io.File;
+
+import org.openstreetmap.josm.data.coor.ILatLon;
+
+/**
+ *
+ * @author Harald Hetzner
+ *
+ */
+public interface HgtDownloadListener {
+
+ /**
+ * Informs the implementing class that downloading of HGT data for the given
+ * coordinates has started.
+ *
+ * To be called by the thread downloading as soon as downloading actually started.
+ *
+ * @param latLon The coordinates for which the HGT data is now being downloaded.
+ */
+ public void hgtFileDownloading(ILatLon latLon);
+
+ /**
+ * Informs the implementing class that HGT data for the given coordinates was
+ * successfully downloaded.
+ *
+ * To be called by the thread downloading as soon as the download finished.
+ *
+ * @param latLon The coordinates for which the download of HGT data succeeded.
+ * @param hgtFile The downloaded HGT file.
+ */
+ public void hgtFileDownloadSucceeded(ILatLon latLon, File hgtFile);
+
+ /**
+ * Informs the implementing class that downloading HGT data for the given
+ * coordinates failed.
+ *
+ * To be called by the thread downloading as soon as downloading fails.
+ *
+ * @param latLon The coordinates for which the download of HGT data failed.
+ */
+ public void hgtFileDownloadFailed(ILatLon latLon);
+
+}
diff --git a/src/org/openstreetmap/josm/data/elevation/HgtDownloader.java b/src/org/openstreetmap/josm/data/elevation/HgtDownloader.java
new file mode 100644
index 000000000..880d74497
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/elevation/HgtDownloader.java
@@ -0,0 +1,163 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.elevation;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.LinkedList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.openstreetmap.josm.data.coor.ILatLon;
+import org.openstreetmap.josm.gui.io.DownloadFileTask;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.tools.Http1Client;
+import org.openstreetmap.josm.tools.Http1Client.Http1Response;
+import org.openstreetmap.josm.tools.HttpClient;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Class {@code HgtDownloader} downloads SRTM HGT (Shuttle Radar Topography Mission Height) files with elevation data.
+ * Currently this class is restricted to a resolution of 3 arc seconds (SRTM3).
+ *
+ * SRTM3 HGT files are available at
+ * NASA's Land Processes Distributed Active Archive Center (LP DAAC).
+ *
+ * In order to access these files, registration at NASA Earthdata Login User Registration
+ * and creating an authorization bearer token on this site are required.
+ *
+ * @author Harald Hetzner
+ * @see HgtReader
+ *
+ */
+public class HgtDownloader {
+
+ private URL baseUrl;
+ private File hgtDirectory;
+
+ private String authHeader;
+
+ private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();
+
+ private LinkedList downloadListeners = new LinkedList();
+
+ public HgtDownloader(File hgtDirectory, String url, String bearer) throws MalformedURLException {
+ // May throw MalformedURLException
+ this.baseUrl = new URL(url);
+ // https://stackoverflow.com/questions/38085964/authorization-bearer-token-in-httpclient
+ if (!bearer.equals(""))
+ authHeader = "Bearer " + bearer;
+ else
+ authHeader = null;
+ this.hgtDirectory = hgtDirectory;
+ }
+
+ public HgtDownloader(File hgtDirectory) throws MalformedURLException {
+ this(hgtDirectory, HgtPreferences.HGT_SERVER_BASE_URL,
+ Config.getPref().get(HgtPreferences.ELEVATION_SERVER_AUTH_BEARER, HgtPreferences.DEFAULT_ELEVATION_SERVER_AUTH_BEARER));
+ }
+
+ public void setHgtDirectory(File hgtDirectory) {
+ this.hgtDirectory = hgtDirectory;
+ }
+
+ public void addDownloadListener(HgtDownloadListener listener) {
+ if (!downloadListeners.contains(listener))
+ downloadListeners.add(listener);
+ }
+
+ public void downloadHgtFile(ILatLon latLon) {
+ EXECUTOR.submit(new DownloadHgtFileTask(latLon));
+ }
+
+ /**
+ * Gets the associated HGT file name for the given coordinate. Usually the
+ * format is [N|S]nn[W|E]mmm.hgt where nn is the integral latitude
+ * without decimals and mmm is the longitude.
+ *
+ * @param latLon The coordinate to get the filename for
+ * @return The file name of the HGT file.
+ */
+ public static String getHgtZipFileName(ILatLon latLon) {
+ return HgtReader.getHgtPrefix(latLon) + HgtPreferences.HGT_ZIP_FILE_PREFIX;
+ }
+
+ private class DownloadHgtFileTask implements Runnable {
+
+ private final ILatLon latLon;
+
+
+ public DownloadHgtFileTask(ILatLon latLon) {
+ this.latLon = latLon;
+ }
+
+ @Override
+ public void run() {
+ downloading();
+ String hgtDirectoryPath = HgtDownloader.this.hgtDirectory.toString();
+ String hgtZipFileName = HgtDownloader.getHgtZipFileName(latLon);
+
+ URL url = null;
+ try {
+ url = new URL(HgtDownloader.this.baseUrl + hgtZipFileName);
+ } catch (MalformedURLException e) {
+ downloadFailed();
+ return;
+ }
+ HttpClient.setFactory(Http1Client::new);
+ Http1Client httpClient = (Http1Client) HttpClient.create(url);
+ if (authHeader != null)
+ httpClient.setHeader("Authorization", authHeader);
+ Http1Response response = null;
+ try {
+ response = (Http1Response) httpClient.connect();
+ //Logging.info("Elevation: HGT server responded: " + response.getResponseCode() + " " + response.getResponseMessage());
+ if (response.getResponseCode() != 200) {
+ downloadFailed();
+ return;
+ }
+ InputStream in = response.getInputStream();
+ Path downloadedZipFile = Paths.get(hgtDirectoryPath, hgtZipFileName);
+ Files.copy(in, downloadedZipFile, StandardCopyOption.REPLACE_EXISTING);
+ DownloadFileTask.unzipFileRecursively(downloadedZipFile.toFile(), hgtDirectoryPath);
+ Files.delete(downloadedZipFile);
+ } catch (IOException e) {
+ if (response != null)
+ Logging.error("Elevation: HGT server responded: " + response.getResponseCode() + " " + response.getResponseMessage());
+ Logging.error("Elevation: Downloading HGT file " + hgtZipFileName + " failed: " + e.toString());
+ downloadFailed();
+ return;
+ }
+ String hgtFileName = HgtReader.getHgtFileName(latLon);
+ File downloadedHgtFile = Paths.get(hgtDirectoryPath, hgtFileName).toFile();
+ if (!downloadedHgtFile.isFile()) {
+ Logging.error("Downloaded HGT file " + downloadedHgtFile.toString() + " is not file !?!");
+ downloadFailed();
+ return;
+ }
+ Logging.info("Elevation: Downloaded and extracted HGT file " + hgtZipFileName + " to elevation directory: " + downloadedHgtFile.toString());
+ downloadSucceeded(downloadedHgtFile);
+ }
+
+ private void downloading() {
+ for (HgtDownloadListener listener : HgtDownloader.this.downloadListeners)
+ listener.hgtFileDownloading(latLon);
+ }
+
+ private void downloadSucceeded(File hgtFile) {
+ for (HgtDownloadListener listener : HgtDownloader.this.downloadListeners)
+ listener.hgtFileDownloadSucceeded(latLon, hgtFile);
+ }
+
+ private void downloadFailed() {
+ for (HgtDownloadListener listener : HgtDownloader.this.downloadListeners)
+ listener.hgtFileDownloadFailed(latLon);
+ }
+ }
+}
diff --git a/src/org/openstreetmap/josm/data/elevation/HgtPreferences.java b/src/org/openstreetmap/josm/data/elevation/HgtPreferences.java
new file mode 100644
index 000000000..6fe4854fe
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/elevation/HgtPreferences.java
@@ -0,0 +1,63 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.elevation;
+
+import java.io.File;
+import java.nio.file.Paths;
+
+import org.openstreetmap.josm.data.Preferences;
+
+/**
+ * Property keys and default values for elevation data preferences.
+ *
+ * @author Harald Hetzner
+ *
+ */
+public class HgtPreferences {
+
+ /** Property key for enabling or disabling use of elevation data. */
+ public static final String ELEVATION_ENABLED = "elevation.enabled";
+
+ /** Property key for enabling or disabling automatic download of elevation data. */
+ public static final String ELEVATION_AUTO_DOWNLOAD_ENABLED = "elevation.autodownload";
+
+ /**
+ * Property key for authentication bearer token for SRTM HGT server.
+ * @see HgtDownloader
+ */
+ public static final String ELEVATION_SERVER_AUTH_BEARER = "elevation.server.auth.bearer";
+
+ /** Default property value for enabling use of elevation data: {@code false}. */
+ public static final boolean DEFAULT_ELEVATION_ENABLED = false;
+
+ /** Default property value for enabling automatic download of elevation data: {@code false}. */
+ public static final boolean DEFAULT_ELEVATION_AUTO_DOWNLOAD_ENABLED = false;
+
+ /** Default property value for authentication bearer token for SRTM HGT server: Empty {@code String}. */
+ public static final String DEFAULT_ELEVATION_SERVER_AUTH_BEARER = "";
+
+ /** Default path, where SRTM3 HGT files need to be placed, respectively to which they will be downloaded. */
+ public static final File DEFAULT_HGT_DIRECTORY = Paths.get(Preferences.main().getDirs().getCacheDirectory(true).toString(), "elevation", "SRTM3").toFile();
+
+ /**
+ * URL of NASA Earthdata Login User Registration
+ * where users need to register and create an authorization bearer token in order to download elevation
+ * data from {@link HgtPreferences#HGT_SERVER_BASE_URL}.
+ */
+ public static final String HGT_SERVER_REGISTRATION_URL = "https://urs.earthdata.nasa.gov/users/new/";
+
+ /**
+ * URL of
+ * NASA's Land Processes Distributed Active Archive Center (LP DAAC)
+ * where SRTM3 HGT files can be downloaded.
+ *
+ * Requires registration at {@link HgtPreferences#HGT_SERVER_REGISTRATION_URL}.
+ */
+ public static final String HGT_SERVER_BASE_URL = "https://e4ftl01.cr.usgs.gov/MEASURES/SRTMGL3.003/2000.02.11/";
+
+ /**
+ * Prefix of compressed as-downloaded HGT files.
+ */
+ public static final String HGT_ZIP_FILE_PREFIX = ".SRTMGL3.hgt.zip";
+
+ private HgtPreferences() {}
+}
diff --git a/src/org/openstreetmap/josm/data/elevation/HgtReader.java b/src/org/openstreetmap/josm/data/elevation/HgtReader.java
new file mode 100644
index 000000000..a8a05efdc
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/elevation/HgtReader.java
@@ -0,0 +1,493 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.elevation;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.compress.utils.IOUtils;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.coor.ILatLon;
+import org.openstreetmap.josm.io.Compression;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Class {@code HgtReader} reads elevation data from SRTM HGT (Shuttle Radar Topography Mission Height) files.
+ * Currently this class is restricted to a resolution of 3 arc seconds (SRTM3).
+ *
+ * SRTM3 HGT files are available at
+ * NASA's Land Processes Distributed Active Archive Center (LP DAAC).
+ *
+ * In order to access these files, registration at NASA Earthdata Login User Registration
+ * and creating an authorization bearer token on this site are required.
+ *
+ * @author Oliver Wieland <oliver.wieland@online.de>
+ * @author Harald Hetzner
+ * @see HgtDownloader
+ */
+public class HgtReader implements HgtDownloadListener {
+ private static final int SRTM_EXTENT = 1; // degree
+ private static final List COMPRESSION_EXT = Arrays.asList("xz", "gzip", "zip", "bz", "bz2");
+
+ public static final String HGT_EXT = ".hgt";
+
+ // alter these values for different SRTM resolutions
+ public static final int HGT_VOID = Short.MIN_VALUE; // magic number which indicates 'void data' in HGT file
+
+ private static final HashMap cache = new HashMap<>();
+
+ private static HgtReader hgtReader = null;
+
+ private File hgtDirectory = null;
+ private boolean autoDownloadEnabled = false;
+ private HgtDownloader hgtDownloader = null;
+
+
+ public static HgtReader getInstance() {
+ if (hgtReader == null)
+ hgtReader = new HgtReader();
+ return hgtReader;
+ }
+
+ public static void destroyInstance() {
+ hgtReader = null;
+ }
+
+ private HgtReader() {
+ this(HgtPreferences.DEFAULT_HGT_DIRECTORY);
+ }
+
+ private HgtReader(File hgtDirectory) {
+ setHgtDirectory(hgtDirectory);
+ }
+
+ public void setHgtDirectory(File hgtDirectory) {
+ if (!hgtDirectory.exists() && hgtDirectory.mkdirs())
+ Logging.info("Elevation: Created directory for HGT files: " + hgtDirectory.toString());
+ if (hgtDirectory.isDirectory()) {
+ this.hgtDirectory = hgtDirectory;
+ Logging.info("Elevation: Set directory for HGT files to: " + hgtDirectory.toString());
+ }
+ else {
+ Logging.error("Elevation: Could not create directory for HGT files: " + hgtDirectory.toString());
+ hgtDirectory = null;
+ }
+ if (hgtDownloader != null)
+ hgtDownloader.setHgtDirectory(hgtDirectory);
+ }
+
+ // TODO: Enable setting the URL, username and password
+ public void setAutoDownloadEnabled(boolean enabled) {
+ if (autoDownloadEnabled == enabled)
+ return;
+ if (enabled) {
+ if (hgtDirectory != null) {
+ if (hgtDownloader == null)
+ try {
+ hgtDownloader = new HgtDownloader(hgtDirectory);
+ hgtDownloader.addDownloadListener(this);
+ } catch (MalformedURLException e) {
+ autoDownloadEnabled = false;
+ Logging.error("Elevation: Cannot enable auto-downloading: " + e.toString());
+ return;
+ }
+ else
+ hgtDownloader.setHgtDirectory(hgtDirectory);
+ autoDownloadEnabled = true;
+ Logging.info("Elevation: Enabled auto-downloading of HGT files to " + hgtDirectory.toString());
+ }
+ else {
+ hgtDownloader = null;
+ autoDownloadEnabled = false;
+ Logging.error("Elevation: Cannot enable auto-downloading as directory for HGT files was not set");
+ }
+ }
+ else {
+ hgtDownloader = null;
+ autoDownloadEnabled = false;
+ Logging.info("Elevation: Disabled auto-downloading of HGT files");
+ }
+ }
+
+ /**
+ * Returns the elevation at the location of the provided coordinate.
+ * If there is not HGT file with elevation data for this location and
+ * autoDownload is enabled, it will be attempted to download
+ * the HGT file.
+ *
+ * @param coor The location at which the elevation is of interest.
+ * @return The elevation at the provided location or {@link ElevationHelper#NO_ELEVATION ElevationHelper.NO_ELEVATION}
+ * if no HGT file elevation data for the location is available at present.
+ */
+ public double getElevationFromHgt(ILatLon coor) {
+ File hgtDirectory = this.hgtDirectory;
+ if (hgtDirectory == null)
+ return ElevationHelper.NO_ELEVATION;
+ String name = getHgtPrefix(coor);
+ String fileName = getHgtFileName(coor);
+ HgtCacheData hgtCacheData;
+ try {
+ synchronized(cache) {
+ hgtCacheData = cache.get(name);
+ // data not in cache
+ if (hgtCacheData == null) {
+ File hgtFile = Paths.get(hgtDirectory.getAbsolutePath(), fileName).toFile();
+ // If a HGT file with the data exists locally, read it in
+ if (hgtFile.exists()) {
+ short[][] data = readHgtFile(hgtFile.toString());
+ hgtCacheData = new HgtCacheData(data);
+ cache.put(name, hgtCacheData);
+ }
+ // Otherwise, put an empty data set with status "missing" into the cache
+ else {
+ hgtCacheData = new HgtCacheData();
+ cache.put(name, hgtCacheData);
+ }
+ }
+ }
+ // Read elevation value if HGT data is available
+ if (hgtCacheData.getStatus() == HgtCacheData.Status.VALID)
+ return readElevation(coor);
+ // If the HGT file with the relevant elevation data is missing and auto-downloading is enabled, try to download it
+ else if (hgtCacheData.getStatus() == HgtCacheData.Status.MISSING && autoDownloadEnabled)
+ hgtDownloader.downloadHgtFile(coor);
+ // If not valid elevation data could be returned, return no elevation
+ return ElevationHelper.NO_ELEVATION;
+ } catch (FileNotFoundException e) {
+ Logging.error("Get elevation from HGT " + coor + " failed: => " + e.getMessage());
+ // no problem... file not there
+ return ElevationHelper.NO_ELEVATION;
+ } catch (Exception ioe) {
+ // oops...
+ Logging.error(ioe);
+ // fallback
+ return ElevationHelper.NO_ELEVATION;
+ }
+ }
+
+ // TODO: This method is not needed to display elevation at current location in JOSM GUI
+ public static Bounds read(File file) throws IOException {
+ String location = file.getName();
+ for (String ext : COMPRESSION_EXT) {
+ location = location.replaceAll("\\." + ext + "$", "");
+ }
+ short[][] data = readHgtFile(file.getPath());
+ // Overwrite the cache file (assume that is desired)
+ HgtCacheData hgtCacheData = new HgtCacheData(data);
+ // TODO: This method does not ensure that "location" derived from file name conforms to the naming pattern
+ cache.put(location, hgtCacheData);
+ Pattern pattern = Pattern.compile("([NS])(\\d{2})([EW])(\\d{3})");
+ Matcher matcher = pattern.matcher(location);
+ if (matcher.lookingAt()) {
+ int lat = ("S".equals(matcher.group(1)) ? -1 : 1) * Integer.parseInt(matcher.group(2));
+ int lon = ("W".equals(matcher.group(3)) ? -1 : 1) * Integer.parseInt(matcher.group(4));
+ return new Bounds(lat, lon, lat + 1, lon + 1);
+ }
+ return null;
+ }
+
+ private static short[][] readHgtFile(String file) throws IOException {
+ CheckParameterUtil.ensureParameterNotNull(file);
+
+ short[][] data = null;
+
+ try (InputStream fis = Compression.getUncompressedFileInputStream(Paths.get(file))) {
+ // choose the right endianness
+ ByteBuffer bb = ByteBuffer.wrap(IOUtils.toByteArray(fis));
+ //System.out.println(Arrays.toString(bb.array()));
+ bb.order(ByteOrder.BIG_ENDIAN);
+ int size = (int) Math.sqrt(bb.array().length / 2);
+ data = new short[size][size];
+ int x = 0;
+ int y = 0;
+ while (x < size) {
+ while (y < size) {
+ data[x][y] = bb.getShort(2 * (x * size + y));
+ y++;
+ }
+ x++;
+ y = 0;
+ }
+ }
+
+ return data;
+ }
+
+ /**
+ * Reads the elevation value for the given coordinate.
+ *
+ * See also stackexchange.com
+ * @param coor the coordinate to get the elevation data for
+ * @return the elevation value or Double.NaN, if no value is present
+ */
+ public static double readElevation(ILatLon coor) {
+ String fileName = getHgtFileName(coor);
+ return readElevation(coor, fileName);
+ }
+
+ /**
+ * Reads the elevation value for the given coordinate.
+ *
+ * See also stackexchange.com
+ * @param coor the coordinate to get the elevation data for
+ * @param fileName The expected filename
+ * @return the elevation value or Double.NaN, if no value is present
+ */
+ public static double readElevation(ILatLon coor, String fileName) {
+ String name = getHgtPrefix(fileName);
+ HgtCacheData hgtCacheData;
+
+ synchronized(cache) {
+ hgtCacheData = cache.get(name);
+ }
+
+ if (hgtCacheData == null || hgtCacheData.getStatus() != HgtCacheData.Status.VALID)
+ return ElevationHelper.NO_ELEVATION;
+
+ short[][] data = hgtCacheData.getData();
+ if (data == null)
+ return ElevationHelper.NO_ELEVATION;
+
+ int[] index = getIndex(coor, data.length);
+ short ele = data[index[0]][index[1]];
+
+ if (ele == HGT_VOID)
+ return ElevationHelper.NO_ELEVATION;
+ return ele;
+ }
+
+ // TODO: This method is not needed to display elevation at current location in JOSM GUI
+ public static Optional getBounds(ILatLon location) {
+ final String fileName = getHgtFileName(location);
+ short[][] sb = null;
+
+ synchronized(cache) {
+ sb = cache.get(fileName).getData();
+ }
+
+ if (sb == null) {
+ return Optional.empty();
+ }
+
+ final double latDegrees = location.lat();
+ final double lonDegrees = location.lon();
+
+ final float fraction = ((float) SRTM_EXTENT) / sb.length;
+ final int latitude = (int) Math.floor(latDegrees) + (latDegrees < 0 ? 1 : 0);
+ final int longitude = (int) Math.floor(lonDegrees) + (lonDegrees < 0 ? 1 : 0);
+
+ final int[] index = getIndex(location, sb.length);
+ final int latSign = latitude > 0 ? 1 : -1;
+ final int lonSign = longitude > 0 ? 1 : -1;
+ final double minLat = latitude + latSign * fraction * index[0];
+ final double maxLat = latitude + latSign * fraction * (index[0] + 1);
+ final double minLon = longitude + lonSign * fraction * index[1];
+ final double maxLon = longitude + lonSign * fraction * (index[1] + 1);
+ return Optional.of(new Bounds(Math.min(minLat, maxLat), Math.min(minLon, maxLon),
+ Math.max(minLat, maxLat), Math.max(minLon, maxLon)));
+ }
+
+ /**
+ * Get the index to use for a short[latitude][longitude] = height in meters array
+ *
+ * @param latLon
+ * The location to get the index for
+ * @param mapSize
+ * The size of the map
+ * @return A [latitude, longitude] = int (index) array.
+ */
+ private static int[] getIndex(ILatLon latLon, int mapSize)
+ {
+ double latDegrees = latLon.lat();
+ double lonDegrees = latLon.lon();
+
+ float fraction = ((float) SRTM_EXTENT) / (mapSize - 1);
+ int latitude = (int) Math.round(frac(latDegrees) / fraction);
+ int longitude = (int) Math.round(frac(lonDegrees) / fraction);
+ if (latDegrees >= 0)
+ {
+ latitude = mapSize - latitude - 1;
+ }
+ if (lonDegrees < 0)
+ {
+ longitude = mapSize - longitude - 1;
+ }
+ return new int[] { latitude, longitude };
+ }
+
+ /**
+ * Gets the associated HGT file name for the given way point. Usually the
+ * format is [N|S]nn[W|E]mmm.hgt where nn is the integral latitude
+ * without decimals and mmm is the longitude.
+ *
+ * @param latLon the coordinate to get the filename for
+ * @return the file name of the HGT file
+ */
+ public static String getHgtFileName(ILatLon latLon) {
+ return getHgtPrefix(latLon) + HGT_EXT;
+ }
+
+ public static String getHgtPrefix(ILatLon latLon) {
+ int lat = (int) Math.floor(latLon.lat());
+ int lon = (int) Math.floor(latLon.lon());
+
+ String latPref = "N";
+ if (lat < 0) {
+ latPref = "S";
+ lat = Math.abs(lat);
+ }
+
+ String lonPref = "E";
+ if (lon < 0) {
+ lonPref = "W";
+ lon = Math.abs(lon);
+ }
+
+ return String.format("%s%2d%s%03d", latPref, lat, lonPref, lon);
+ }
+
+ public static String getHgtPrefix(String fileName) {
+ return fileName.replace(".hgt", "");
+ }
+
+ public static double frac(double d) {
+ long iPart;
+ double fPart;
+
+ // Get user input
+ iPart = (long) d;
+ fPart = d - iPart;
+ return fPart;
+ }
+
+ public static void clearCache() {
+ synchronized(cache) {
+ cache.clear();
+ }
+ }
+
+ @Override
+ public void hgtFileDownloading(ILatLon latLon) {
+ String name = getHgtPrefix(latLon);
+
+ synchronized (cache) {
+ HgtCacheData hgtCacheData = cache.get(name);
+ // Should not happen
+ if (hgtCacheData == null) {
+ hgtCacheData = new HgtCacheData();
+ cache.put(name, hgtCacheData);
+ }
+ hgtCacheData.setDownloading();
+ }
+ }
+
+ @Override
+ public void hgtFileDownloadSucceeded(ILatLon latLon, File hgtFile) {
+ String name = getHgtPrefix(latLon);
+ short[][] data = null;
+ try {
+ data = readHgtFile(hgtFile.getAbsolutePath());
+ } catch (Exception e) {
+ Logging.error("Elevation: Error reading HGT file " + hgtFile.getAbsolutePath() + " :" + e.toString());
+ }
+
+ HgtCacheData hgtCacheData;
+ synchronized (cache) {
+ hgtCacheData = cache.get(name);
+ if (hgtCacheData != null)
+ hgtCacheData.setData(data);
+ // Should not happen
+ else {
+ hgtCacheData = new HgtCacheData(data);
+ cache.put(name,hgtCacheData);
+ }
+ }
+
+ // In case that the downloaded file is corrupt, try to delete it
+ if (data == null) {
+ hgtCacheData.setDownloadFailed();
+ Logging.info("Elevation: Deleting downloaeded, but corrupt HGT file: " + hgtFile.getAbsolutePath());
+ try {
+ Files.delete(Paths.get(hgtFile.getAbsolutePath()));
+ } catch (IOException e) {
+ Logging.error("Elevation: Error deleting downloaded, but corrupt HGT file: " + e.toString());
+ }
+ }
+ }
+
+ @Override
+ public void hgtFileDownloadFailed(ILatLon latLon) {
+ String name = getHgtPrefix(latLon);
+
+ synchronized (cache) {
+ HgtCacheData hgtCacheData = cache.get(name);
+ // Should not happen
+ if (hgtCacheData == null) {
+ hgtCacheData = new HgtCacheData();
+ cache.put(name, hgtCacheData);
+ }
+ hgtCacheData.setDownloadFailed();
+ }
+
+ }
+
+ private static class HgtCacheData {
+
+ private short[][] data;
+ private Status status;
+
+ enum Status {
+ VALID,
+ MISSING,
+ DOWNLOADING,
+ DOWNLOAD_FAILED
+ }
+
+ public HgtCacheData() {
+ this(null);
+ }
+
+ public HgtCacheData(short[][] data) {
+ setData(data);
+ }
+
+ public short[][] getData() {
+ return data;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public void setDownloading() {
+ status = Status.DOWNLOADING;
+ data = null;
+ }
+
+ public void setDownloadFailed() {
+ status = Status.DOWNLOAD_FAILED;
+ data = null;
+ }
+
+ public void setData(short[][] data) {
+ if (data == null)
+ status = Status.MISSING;
+ else
+ status = Status.VALID;
+ this.data = data;
+ }
+ }
+}
diff --git a/src/org/openstreetmap/josm/data/elevation/gpx/GeoidCorrectionKind.java b/src/org/openstreetmap/josm/data/elevation/gpx/GeoidCorrectionKind.java
new file mode 100644
index 000000000..5b555716e
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/elevation/gpx/GeoidCorrectionKind.java
@@ -0,0 +1,15 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.elevation.gpx;
+
+/**
+ * @author Oliver Wieland <oliver.wieland@online.de>
+ * Enumeration for available elevation correction modes.
+ */
+public enum GeoidCorrectionKind {
+ /** Elevation values remain unchanged */
+ None,
+ /** Automatic correction by geoid lookup table */
+ Auto,
+ /** Fixed value */
+ Fixed
+}
diff --git a/src/org/openstreetmap/josm/gui/MapStatus.java b/src/org/openstreetmap/josm/gui/MapStatus.java
index 83ba6364b..2d7b01343 100644
--- a/src/org/openstreetmap/josm/gui/MapStatus.java
+++ b/src/org/openstreetmap/josm/gui/MapStatus.java
@@ -67,6 +67,9 @@ import org.openstreetmap.josm.data.coor.conversion.CoordinateFormatManager;
import org.openstreetmap.josm.data.coor.conversion.DMSCoordinateFormat;
import org.openstreetmap.josm.data.coor.conversion.ICoordinateFormat;
import org.openstreetmap.josm.data.coor.conversion.ProjectedCoordinateFormat;
+import org.openstreetmap.josm.data.elevation.ElevationHelper;
+import org.openstreetmap.josm.data.elevation.HgtPreferences;
+import org.openstreetmap.josm.data.elevation.HgtReader;
import org.openstreetmap.josm.data.osm.DataSelectionListener;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
@@ -124,6 +127,7 @@ public final class MapStatus extends JPanel implements
Helpful, Destroyable, PreferenceChangedListener, SoMChangeListener, DataSelectionListener, DataSetListener, ZoomChangeListener {
private final DecimalFormat DECIMAL_FORMAT = new DecimalFormat(Config.getPref().get("statusbar.decimal-format", "0.00"));
+ private final DecimalFormat ELEVATION_FORMAT = new DecimalFormat("0 m");
private static final AbstractProperty DISTANCE_THRESHOLD = new DoubleProperty("statusbar.distance-threshold", 0.01).cached();
private static final AbstractProperty SHOW_ID = new BooleanProperty("osm-primitives.showid", false);
@@ -246,6 +250,8 @@ public final class MapStatus extends JPanel implements
null, DMSCoordinateFormat.INSTANCE.latToString(LatLon.SOUTH_POLE).length(), PROP_BACKGROUND_COLOR.get());
private final ImageLabel lonText = new ImageLabel("lon",
null, DMSCoordinateFormat.INSTANCE.lonToString(new LatLon(0, 180)).length(), PROP_BACKGROUND_COLOR.get());
+ private final ImageLabel eleText = new ImageLabel("ele",
+ tr("The terrain elevation at the mouse pointer."), 10, PROP_BACKGROUND_COLOR.get());
private final ImageLabel headingText = new ImageLabel("heading",
tr("The (compass) heading of the line segment being drawn."),
DECIMAL_FORMAT.format(360).length() + 1, PROP_BACKGROUND_COLOR.get());
@@ -279,6 +285,8 @@ public final class MapStatus extends JPanel implements
private final transient List statusText = new ArrayList<>();
+ private boolean elevationEnabled = Config.getPref().getBoolean(HgtPreferences.ELEVATION_ENABLED, HgtPreferences.DEFAULT_ELEVATION_ENABLED);
+
protected static final class StatusTextHistory {
private final Object id;
private final String text;
@@ -912,6 +920,7 @@ public final class MapStatus extends JPanel implements
latText.setForeground(PROP_FOREGROUND_COLOR.get());
lonText.setForeground(PROP_FOREGROUND_COLOR.get());
+ eleText.setForeground(PROP_FOREGROUND_COLOR.get());
headingText.setForeground(PROP_FOREGROUND_COLOR.get());
distText.setForeground(PROP_FOREGROUND_COLOR.get());
nameText.setForeground(PROP_FOREGROUND_COLOR.get());
@@ -925,6 +934,8 @@ public final class MapStatus extends JPanel implements
add(latText, GBC.std());
add(lonText, GBC.std().insets(3, 0, 0, 0));
add(headingText, GBC.std().insets(3, 0, 0, 0));
+ add(eleText, GBC.std().insets(3, 0, 0, 0));
+ eleText.setVisible(elevationEnabled);
add(angleText, GBC.std().insets(3, 0, 0, 0));
add(distText, GBC.std().insets(3, 0, 0, 0));
@@ -972,6 +983,11 @@ public final class MapStatus extends JPanel implements
};
mv.addComponentListener(mvComponentAdapter);
+ if (elevationEnabled) {
+ boolean elevationAutoDownloadEnabled = Config.getPref().getBoolean(HgtPreferences.ELEVATION_AUTO_DOWNLOAD_ENABLED, HgtPreferences.DEFAULT_ELEVATION_AUTO_DOWNLOAD_ENABLED);
+ HgtReader.getInstance().setAutoDownloadEnabled(elevationAutoDownloadEnabled);
+ }
+
// The background thread
thread = new Thread(collector, "Map Status Collector");
thread.setDaemon(true);
@@ -998,6 +1014,16 @@ public final class MapStatus extends JPanel implements
lonText.setToolTipText(tr("The geographic longitude at the mouse pointer."));
previousCoordinateFormat = mCord;
}
+ if (elevationEnabled)
+ updateEleText(p);
+ }
+
+ private void updateEleText(ILatLon coor) {
+ double ele = HgtReader.getInstance().getElevationFromHgt(coor);
+ if (ElevationHelper.isValidElevation(ele))
+ eleText.setText(ELEVATION_FORMAT.format(ele));
+ else
+ eleText.setText(tr("No data"));
}
@Override
@@ -1299,4 +1325,14 @@ public final class MapStatus extends JPanel implements
autoLength = b;
}
+ /**
+ * Enable or disable displaying elevation at the position of the mouse pointer.
+ * @param enabled If {@code true} displaying of elevation is enabled, else disabled.
+ * @see HgtReader
+ */
+ public void setElevationEnabled(boolean enabled) {
+ elevationEnabled = enabled;
+ eleText.setVisible(enabled);
+ }
+
}
diff --git a/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java b/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java
index bb311319e..8eca8947e 100644
--- a/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java
+++ b/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java
@@ -53,6 +53,7 @@ import org.openstreetmap.josm.gui.preferences.display.DrawingPreference;
import org.openstreetmap.josm.gui.preferences.display.GPXPreference;
import org.openstreetmap.josm.gui.preferences.display.LafPreference;
import org.openstreetmap.josm.gui.preferences.display.LanguagePreference;
+import org.openstreetmap.josm.gui.preferences.elevation.HgtPreference;
import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference;
import org.openstreetmap.josm.gui.preferences.map.BackupPreference;
import org.openstreetmap.josm.gui.preferences.map.MapPaintPreference;
@@ -418,6 +419,14 @@ public final class PreferenceTabbedPane extends JTabbedPane implements ExpertMod
return getSetting(ServerAccessPreference.class);
}
+ /**
+ * Returns the {@code HgtServerAccessPreference} object.
+ * @return the {@code HgtServerAccessPreference} object.
+ */
+ public HgtPreference getHgtServerPreference() {
+ return getSetting(HgtPreference.class);
+ }
+
/**
* Returns the {@code ValidatorPreference} object.
* @return the {@code ValidatorPreference} object.
@@ -621,6 +630,7 @@ public final class PreferenceTabbedPane extends JTabbedPane implements ExpertMod
SETTINGS_FACTORIES.add(new ValidatorTagCheckerRulesPreference.Factory());
SETTINGS_FACTORIES.add(new RemoteControlPreference.Factory());
SETTINGS_FACTORIES.add(new ImageryPreference.Factory());
+ SETTINGS_FACTORIES.add(new HgtPreference.Factory());
}
/**
diff --git a/src/org/openstreetmap/josm/gui/preferences/elevation/HgtPreference.java b/src/org/openstreetmap/josm/gui/preferences/elevation/HgtPreference.java
new file mode 100644
index 000000000..13a392a14
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/preferences/elevation/HgtPreference.java
@@ -0,0 +1,80 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.elevation;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import javax.swing.Box;
+
+import org.openstreetmap.josm.data.elevation.HgtPreferences;
+import org.openstreetmap.josm.data.elevation.HgtReader;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting;
+import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
+import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.spi.preferences.IPreferences;
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * Elevation data sub-preferences in preferences.
+ * @author Harald Hetzner
+ *
+ */
+public final class HgtPreference extends DefaultTabPreferenceSetting {
+
+ /**
+ * Factory used to create a new {@code HgtPreference}.
+ */
+ public static class Factory implements PreferenceSettingFactory {
+ @Override
+ public PreferenceSetting createPreferenceSetting() {
+ return new HgtPreference();
+ }
+ }
+
+ private HgtPreferencesPanel pnlHgtPreferences;
+
+ private HgtPreference() {
+ super(/* ICON(preferences/) */ "elevation", tr("Elevation Data"), tr("Elevation preferences and connection settings for the HGT server."));
+ }
+
+ @Override
+ public void addGui(PreferenceTabbedPane gui) {
+ pnlHgtPreferences = new HgtPreferencesPanel();
+ pnlHgtPreferences.add(Box.createVerticalGlue(), GBC.eol().fill());
+ gui.createPreferenceTab(this).add(pnlHgtPreferences, GBC.eol().fill());
+ }
+
+ /**
+ * Saves the values to the preferences and applies them.
+ */
+ @Override
+ public boolean ok() {
+ // Save to preferences file
+ pnlHgtPreferences.saveToPreferences();
+
+ // Apply preferences
+ IPreferences pref = Config.getPref();
+ boolean elevationEnabled = pref.getBoolean(HgtPreferences.ELEVATION_ENABLED, HgtPreferences.DEFAULT_ELEVATION_ENABLED);
+ if (elevationEnabled) {
+ boolean elevationAutoDownloadEnabled = pref.getBoolean(HgtPreferences.ELEVATION_AUTO_DOWNLOAD_ENABLED, HgtPreferences.DEFAULT_ELEVATION_AUTO_DOWNLOAD_ENABLED);
+ // If enabled, HgtDownloader used by HgtReader will by itself read the authentication bearer token from the preferences
+ HgtReader.getInstance().setAutoDownloadEnabled(elevationAutoDownloadEnabled);
+ }
+ else {
+ HgtReader.destroyInstance();
+ }
+ if (MainApplication.getMap() != null && MainApplication.getMap().statusLine != null)
+ MainApplication.getMap().statusLine.setElevationEnabled(elevationEnabled);
+
+ return false;
+ }
+
+ @Override
+ public String getHelpContext() {
+ // TODO: Add help
+ //return HelpUtil.ht("/Preferences/Elevation");
+ return null;
+ }
+}
diff --git a/src/org/openstreetmap/josm/gui/preferences/elevation/HgtPreferencesPanel.java b/src/org/openstreetmap/josm/gui/preferences/elevation/HgtPreferencesPanel.java
new file mode 100644
index 000000000..3f869b05e
--- /dev/null
+++ b/src/org/openstreetmap/josm/gui/preferences/elevation/HgtPreferencesPanel.java
@@ -0,0 +1,178 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.preferences.elevation;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Desktop;
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.io.IOException;
+import java.net.URI;
+
+import javax.swing.BorderFactory;
+import javax.swing.JCheckBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.event.HyperlinkEvent;
+
+import org.openstreetmap.josm.data.elevation.HgtPreferences;
+import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
+import org.openstreetmap.josm.gui.widgets.JosmTextField;
+import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.spi.preferences.IPreferences;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Component allowing input of HGT (elevation) server settings.
+ */
+public class HgtPreferencesPanel extends VerticallyScrollablePanel {
+
+ static final class AutoSizePanel extends JPanel {
+ AutoSizePanel() {
+ super(new GridBagLayout());
+ }
+
+ @Override
+ public Dimension getMinimumSize() {
+ return getPreferredSize();
+ }
+ }
+
+ private final JCheckBox cbEnableElevation = new JCheckBox(tr("Enable Use of Elevation Data"));
+ private final JMultilineLabel lblpElevationData =
+ new JMultilineLabel(tr("STRM3 HGT files can be downloaded from {0}.", HgtPreferences.HGT_SERVER_BASE_URL));
+ private final JCheckBox cbEnableAutoDownload = new JCheckBox(tr("Enable Automatic Downloading of Elevation Data"));
+ private final JLabel lblAuthBearer = new JLabel(tr("Authorization Bearer Token:"));
+ private final JosmTextField tfAuthBearer = new JosmTextField();
+ private final JMultilineLabel lblAuthBearerNotes =
+ new JMultilineLabel(tr("You need to register at {0} to create the authorization bearer token.",
+ HgtPreferences.HGT_SERVER_REGISTRATION_URL));
+
+ /**
+ * Builds the panel for the elevation preferences.
+ *
+ * @return Panel with elevation preferences
+ */
+ protected final JPanel buildHgtPreferencesPanel() {
+ cbEnableElevation.setToolTipText(tr("STRM3 HGT files need to be placed in {0}", HgtPreferences.DEFAULT_HGT_DIRECTORY.getAbsolutePath()));
+ cbEnableElevation.addItemListener(event -> updateEnabledState());
+
+ lblpElevationData.setEditable(false);
+ lblpElevationData.addHyperlinkListener(event -> browseHyperlink(event));
+
+ cbEnableAutoDownload.setToolTipText(tr("STRM3 HGT files will be downloaded from {0}", HgtPreferences.HGT_SERVER_BASE_URL));
+ cbEnableAutoDownload.addItemListener(event -> updateEnabledState());
+
+ lblAuthBearerNotes.setEditable(false);
+ lblAuthBearerNotes.addHyperlinkListener(event -> browseHyperlink(event));
+
+ JPanel pnl = new AutoSizePanel();
+ GridBagConstraints gc = new GridBagConstraints();
+
+ gc.anchor = GridBagConstraints.LINE_START;
+ gc.insets = new Insets(5, 5, 0, 0);
+ gc.fill = GridBagConstraints.HORIZONTAL;
+ gc.gridwidth = 2;
+ gc.weightx = 1.0;
+ pnl.add(cbEnableElevation, gc);
+
+ gc.gridy = 1;
+ pnl.add(lblpElevationData, gc);
+
+ gc.gridy = 2;
+ pnl.add(cbEnableAutoDownload, gc);
+
+ gc.gridy = 3;
+ gc.fill = GridBagConstraints.NONE;
+ gc.gridwidth = 1;
+ gc.weightx = 0.0;
+ pnl.add(lblAuthBearer, gc);
+
+ gc.gridx = 1;
+ gc.fill = GridBagConstraints.HORIZONTAL;
+ gc.weightx = 1.0;
+ pnl.add(tfAuthBearer, gc);
+
+ gc.gridy = 4;
+ gc.gridx = 0;
+ gc.gridwidth = 2;
+ gc.weightx = 1.0;
+ pnl.add(lblAuthBearerNotes, gc);
+
+ // add an extra spacer, otherwise the layout is broken
+ gc.gridy = 5;
+ gc.gridwidth = 2;
+ gc.fill = GridBagConstraints.BOTH;
+ gc.weighty = 1.0;
+ pnl.add(new JPanel(), gc);
+ return pnl;
+ }
+
+
+ /**
+ * Initializes the panel with the values from the preferences.
+ */
+ public final void initFromPreferences() {
+ IPreferences pref = Config.getPref();
+
+ cbEnableElevation.setSelected(pref.getBoolean(HgtPreferences.ELEVATION_ENABLED, HgtPreferences.DEFAULT_ELEVATION_ENABLED));
+ cbEnableAutoDownload.setSelected(pref.getBoolean(HgtPreferences.ELEVATION_AUTO_DOWNLOAD_ENABLED, HgtPreferences.DEFAULT_ELEVATION_AUTO_DOWNLOAD_ENABLED));
+ tfAuthBearer.setText(pref.get(HgtPreferences.ELEVATION_SERVER_AUTH_BEARER, HgtPreferences.DEFAULT_ELEVATION_SERVER_AUTH_BEARER));
+ }
+
+ private final void updateEnabledState() {
+ if (cbEnableElevation.isSelected()) {
+ lblpElevationData.setEnabled(true);
+ cbEnableAutoDownload.setEnabled(true);
+ lblAuthBearer.setEnabled(cbEnableAutoDownload.isSelected());
+ tfAuthBearer.setEnabled(cbEnableAutoDownload.isSelected());
+ lblAuthBearerNotes.setEnabled(cbEnableAutoDownload.isSelected());
+ }
+ else {
+ lblpElevationData.setEnabled(false);
+ cbEnableAutoDownload.setEnabled(false);
+ lblAuthBearer.setEnabled(false);
+ tfAuthBearer.setEnabled(false);
+ lblAuthBearerNotes.setEnabled(false);
+ }
+ }
+
+ // https://stackoverflow.com/questions/14101000/hyperlink-to-open-in-browser-in-java
+ // https://www.codejava.net/java-se/swing/how-to-create-hyperlink-with-jlabel-in-java-swing
+ private final void browseHyperlink(HyperlinkEvent event) {
+ if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
+ String url = event.getURL().toString();
+ try {
+ Desktop.getDesktop().browse(URI.create(url));
+ } catch (IOException e) {
+ Logging.error(e.toString());
+ }
+ }
+ }
+
+ /**
+ * Constructs a new {@code HgtPreferencesPanel}.
+ */
+ public HgtPreferencesPanel() {
+ setLayout(new GridBagLayout());
+ setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+ add(buildHgtPreferencesPanel(), GBC.eop().anchor(GridBagConstraints.NORTHWEST).fill(GridBagConstraints.BOTH));
+
+ initFromPreferences();
+ updateEnabledState();
+ }
+
+ /**
+ * Saves the current values to the preferences
+ */
+ public void saveToPreferences() {
+ IPreferences pref = Config.getPref();
+ pref.putBoolean(HgtPreferences.ELEVATION_ENABLED, cbEnableElevation.isSelected());
+ pref.putBoolean(HgtPreferences.ELEVATION_AUTO_DOWNLOAD_ENABLED, cbEnableAutoDownload.isSelected());
+ pref.put(HgtPreferences.ELEVATION_SERVER_AUTH_BEARER, tfAuthBearer.getText());
+ }
+}