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 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + 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()); + } +}