Ticket #22596: JOSM_18616_Elevation_git_diff.txt

File JOSM_18616_Elevation_git_diff.txt, 63.8 KB (added by hhtznr, 3 years ago)

Git diff of elevation patch

Line 
1diff --git a/resources/images/preferences/elevation.svg b/resources/images/preferences/elevation.svg
2new file mode 100644
3index 000000000..cab04bbc6
4--- /dev/null
5+++ b/resources/images/preferences/elevation.svg
6@@ -0,0 +1,89 @@
7+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
8+<svg
9+ width="24"
10+ height="24"
11+ viewBox="0 0 24 24"
12+ id="svg2"
13+ version="1.1"
14+ inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
15+ sodipodi:docname="elevation.svg"
16+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
17+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
18+ xmlns="http://www.w3.org/2000/svg"
19+ xmlns:svg="http://www.w3.org/2000/svg"
20+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
21+ xmlns:cc="http://creativecommons.org/ns#"
22+ xmlns:dc="http://purl.org/dc/elements/1.1/">
23+ <defs
24+ id="defs4" />
25+ <sodipodi:namedview
26+ id="base"
27+ pagecolor="#ffffff"
28+ bordercolor="#666666"
29+ borderopacity="1.0"
30+ inkscape:pageopacity="0.0"
31+ inkscape:pageshadow="2"
32+ inkscape:zoom="56.536771"
33+ inkscape:cx="14.397709"
34+ inkscape:cy="13.362985"
35+ inkscape:document-units="px"
36+ inkscape:current-layer="layer1"
37+ showgrid="true"
38+ units="px"
39+ inkscape:window-width="3770"
40+ inkscape:window-height="2055"
41+ inkscape:window-x="0"
42+ inkscape:window-y="0"
43+ inkscape:window-maximized="1"
44+ viewbox-height="16"
45+ inkscape:document-rotation="0"
46+ inkscape:pagecheckerboard="0"
47+ showguides="true"
48+ inkscape:guide-bbox="true">
49+ <inkscape:grid
50+ type="xygrid"
51+ id="grid4136"
52+ originx="0"
53+ originy="0"
54+ spacingx="1"
55+ spacingy="1" />
56+ </sodipodi:namedview>
57+ <metadata
58+ id="metadata7">
59+ <rdf:RDF>
60+ <cc:Work
61+ rdf:about="">
62+ <dc:format>image/svg+xml</dc:format>
63+ <dc:type
64+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
65+ <cc:license
66+ rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" />
67+ </cc:Work>
68+ <cc:License
69+ rdf:about="http://creativecommons.org/publicdomain/zero/1.0/">
70+ <cc:permits
71+ rdf:resource="http://creativecommons.org/ns#Reproduction" />
72+ <cc:permits
73+ rdf:resource="http://creativecommons.org/ns#Distribution" />
74+ <cc:permits
75+ rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
76+ </cc:License>
77+ </rdf:RDF>
78+ </metadata>
79+ <g
80+ inkscape:label="Layer 1"
81+ inkscape:groupmode="layer"
82+ id="layer1"
83+ transform="translate(0,-1037.3622)">
84+ <path
85+ style="fill:#89a02c;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
86+ d="M 23,1055.3622 H 1 l 5,-7 5,4 6,-10 6,10 z"
87+ id="path1652"
88+ sodipodi:nodetypes="ccccccc" />
89+ <path
90+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
91+ d="m 1,1055.3622 5,-7 5,4 6,-10 6,10"
92+ id="path1617"
93+ sodipodi:nodetypes="ccccc" />
94+ </g>
95+</svg>
96diff --git a/resources/images/statusline/ele.svg b/resources/images/statusline/ele.svg
97new file mode 100644
98index 000000000..4a8ac7392
99--- /dev/null
100+++ b/resources/images/statusline/ele.svg
101@@ -0,0 +1,58 @@
102+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
103+<svg
104+ version="1.1"
105+ width="18px"
106+ height="18px"
107+ viewBox="0 0 18 18"
108+ fill="none"
109+ id="svg6"
110+ sodipodi:docname="ele.svg"
111+ inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
112+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
113+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
114+ xmlns="http://www.w3.org/2000/svg"
115+ xmlns:svg="http://www.w3.org/2000/svg">
116+ <defs
117+ id="defs10" />
118+ <sodipodi:namedview
119+ id="namedview8"
120+ pagecolor="#ffffff"
121+ bordercolor="#666666"
122+ borderopacity="1.0"
123+ inkscape:pageshadow="2"
124+ inkscape:pageopacity="0.0"
125+ inkscape:pagecheckerboard="0"
126+ showgrid="false"
127+ showguides="true"
128+ inkscape:guide-bbox="true"
129+ inkscape:snap-bbox="true"
130+ inkscape:bbox-paths="true"
131+ inkscape:bbox-nodes="true"
132+ inkscape:snap-bbox-edge-midpoints="true"
133+ inkscape:snap-bbox-midpoints="true"
134+ inkscape:object-paths="true"
135+ inkscape:snap-intersection-paths="true"
136+ inkscape:snap-smooth-nodes="true"
137+ inkscape:snap-midpoints="true"
138+ inkscape:snap-object-midpoints="true"
139+ inkscape:snap-center="true"
140+ inkscape:snap-page="true"
141+ inkscape:zoom="48.5"
142+ inkscape:cx="4.556701"
143+ inkscape:cy="9"
144+ inkscape:window-width="3770"
145+ inkscape:window-height="2055"
146+ inkscape:window-x="0"
147+ inkscape:window-y="0"
148+ inkscape:window-maximized="1"
149+ inkscape:current-layer="svg6" />
150+ <path
151+ d="M 9,1 V 17"
152+ stroke="#ee4422"
153+ stroke-width="1.88562"
154+ id="path2" />
155+ <path
156+ style="fill:none;stroke:#000000;stroke-width:0.93133px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
157+ d="M 0.99621412,16.121325 9,2.3402182 17.003786,16.121325 Z"
158+ id="path872" />
159+</svg>
160diff --git a/src/org/openstreetmap/josm/data/elevation/ElevationHelper.java b/src/org/openstreetmap/josm/data/elevation/ElevationHelper.java
161new file mode 100644
162index 000000000..a0cfe7d59
163--- /dev/null
164+++ b/src/org/openstreetmap/josm/data/elevation/ElevationHelper.java
165@@ -0,0 +1,283 @@
166+// License: GPL. For details, see LICENSE file.
167+package org.openstreetmap.josm.data.elevation;
168+
169+import java.util.ArrayList;
170+import java.util.Calendar;
171+import java.util.GregorianCalendar;
172+import java.util.List;
173+import java.util.Optional;
174+
175+import org.openstreetmap.josm.data.Bounds;
176+import org.openstreetmap.josm.data.SystemOfMeasurement;
177+import org.openstreetmap.josm.data.coor.ILatLon;
178+import org.openstreetmap.josm.data.coor.LatLon;
179+import org.openstreetmap.josm.data.elevation.gpx.GeoidCorrectionKind;
180+import org.openstreetmap.josm.data.gpx.WayPoint;
181+import org.openstreetmap.josm.tools.Logging;
182+
183+/**
184+ * Provides methods to access way point attributes and some utility methods regarding elevation stuff (
185+ * e. g. special text formats, unit conversion, geoid calc).
186+ * @author Oliver Wieland &lt;oliver.wieland@online.de&gt;
187+ */
188+public final class ElevationHelper {
189+
190+ private ElevationHelper() {
191+ // Hide default constructor for utilities classes
192+ }
193+
194+ /**
195+ * The 'no elevation' data magic.
196+ * @see ElevationHelper#isValidElevation
197+ */
198+ public static final double NO_ELEVATION = Double.NaN;
199+
200+ /**
201+ * The name of the elevation height of a way point.
202+ */
203+ public static final String HEIGHT_ATTRIBUTE = "ele";
204+
205+ private static GeoidCorrectionKind geoidKind = GeoidCorrectionKind.None;
206+
207+ /**
208+ * Gets the current mode of GEOID correction.
209+ */
210+ public static GeoidCorrectionKind getGeoidKind() {
211+ return geoidKind;
212+ }
213+
214+ public static void setGeoidKind(GeoidCorrectionKind geoidKind) {
215+ ElevationHelper.geoidKind = geoidKind;
216+ }
217+
218+ /**
219+ * Checks if given value is a valid elevation value.
220+ *
221+ * @param ele the ele
222+ * @return true, if is valid elevation
223+ * @see ElevationHelper#NO_ELEVATION
224+ */
225+ public static boolean isValidElevation(double ele) {
226+ return !Double.isNaN(ele);
227+ }
228+
229+ /**
230+ * Gets the elevation (Z coordinate) of a GPX way point in meter or feet (for
231+ * US, UK, ZA, AU, NZ and CA).
232+ *
233+ * @param wpt
234+ * The way point instance.
235+ * @return The x coordinate or <code>NO_ELEVATION</code>, if the given way point is null or contains
236+ * not height attribute.
237+ */
238+ public static double getElevation(WayPoint wpt) {
239+ if (wpt == null) return NO_ELEVATION;
240+
241+ // try to get elevation from HGT file
242+ double eleInt = getSrtmElevation(wpt.getCoor());
243+ if (isValidElevation(eleInt)) {
244+ return eleInt;
245+ }
246+
247+ // no HGT, check for elevation data in GPX
248+ if (!wpt.attr.containsKey(HEIGHT_ATTRIBUTE)) {
249+ // GPX has no elevation data :-(
250+ return NO_ELEVATION;
251+ }
252+
253+ // Parse elevation from GPX data
254+ String height = wpt.getString(ElevationHelper.HEIGHT_ATTRIBUTE);
255+ try {
256+ return Double.parseDouble(height);
257+ } catch (NumberFormatException e) {
258+ Logging.error(String.format("Cannot parse double from '%s': %s", height, e.getMessage()));
259+ return NO_ELEVATION;
260+ }
261+ }
262+
263+ private static double getElevation(LatLon ll) {
264+ return getSrtmElevation(ll);
265+ }
266+
267+ /**
268+ * Computes the slope <b>in percent</b> between two way points. E. g. an elevation gain of 12m
269+ * within a distance of 100m is equal to a slope of 12%.
270+ *
271+ * @param w1 the first way point
272+ * @param w2 the second way point
273+ * @return the slope in percent
274+ */
275+ public static double computeSlope(LatLon w1, LatLon w2) {
276+ // same coordinates? -> return 0, if yes
277+ if (w1.equals(w2)) return 0;
278+
279+ // get distance in meters and divide it by 100 in advance
280+ double distInMeter = w1.greatCircleDistance((ILatLon) w2) / 100.0;
281+
282+ // get elevation (difference)
283+ int ele1 = (int) ElevationHelper.getElevation(w1);
284+ int ele2 = (int) ElevationHelper.getElevation(w2);
285+ int dH = ele2 - ele1;
286+
287+ // Slope in percent is define as elevation gain/loss in meters related to a distance of 100m
288+ return dH / distInMeter;
289+ }
290+
291+ /**
292+ * Gets the elevation string for a given elevation, e. g "300m" or "800ft".
293+ */
294+ public static String getElevationText(int elevation) {
295+ return SystemOfMeasurement.getSystemOfMeasurement().getDistText(elevation);
296+ }
297+
298+ /**
299+ * Gets the elevation string for a given elevation, e. g "300m" or "800ft".
300+ */
301+ public static String getElevationText(double elevation) {
302+ return SystemOfMeasurement.getSystemOfMeasurement().getDistText((int) Math.round(elevation));
303+ }
304+
305+ /**
306+ * Gets the elevation string for a given way point, e. g "300m" or "800ft".
307+ *
308+ * @param wpt the way point
309+ * @return the elevation text
310+ */
311+ public static String getElevationText(WayPoint wpt) {
312+ if (wpt == null) return "-";
313+
314+ return getElevationText(ElevationHelper.getElevation(wpt));
315+ }
316+
317+ /**
318+ * Get the time string for a given way point.
319+ */
320+ public static String getTimeText(WayPoint wpt) {
321+ if (wpt == null) return null;
322+
323+ int hour = ElevationHelper.getHourOfWayPoint(wpt);
324+ int min = ElevationHelper.getMinuteOfWayPoint(wpt);
325+ return String.format("%02d:%02d", hour, min);
326+ }
327+
328+ /**
329+ * Gets the SRTM elevation (Z coordinate) of the given coordinate.
330+ *
331+ * @param ll
332+ * The coordinate.
333+ * @return The z coordinate or {@link Double#NaN}, if elevation value could not be obtained
334+ * not height attribute.
335+ */
336+ public static double getSrtmElevation(ILatLon ll) {
337+ if (ll != null) {
338+ // Try to read data from SRTM file
339+ // TODO: Option to switch this off
340+ double eleHgt = HgtReader.getInstance().getElevationFromHgt(ll);
341+
342+ if (isValidElevation(eleHgt)) {
343+ return eleHgt;
344+ }
345+ }
346+ return NO_ELEVATION;
347+ }
348+
349+ /**
350+ * Get the bounds for the pixel elevation for the latitude
351+ * @param location The location to get
352+ * @return The bounds for the elevation area
353+ */
354+ public static Optional<Bounds> getBounds(ILatLon location) {
355+ if (location != null) {
356+ return HgtReader.getBounds(location);
357+ }
358+ return Optional.empty();
359+ }
360+
361+ /**
362+ * Checks given area for SRTM data.
363+ *
364+ * @param bounds the bounds/area to check
365+ * @return true, if SRTM data are present; otherwise false
366+ */
367+ public static boolean hasSrtmData(Bounds bounds) {
368+ if (bounds == null) return false;
369+
370+ LatLon tl = bounds.getMin();
371+ LatLon br = bounds.getMax();
372+
373+ return isValidElevation(getSrtmElevation(tl)) &&
374+ isValidElevation(getSrtmElevation(br));
375+ }
376+
377+ /*
378+ * Gets the geoid height for the given way point. See also {@link
379+ * GeoidData}.
380+ */
381+ public static byte getGeoidCorrection(WayPoint wpt) {
382+ /*
383+ int lat = (int)Math.round(wpt.getCoor().lat());
384+ int lon = (int)Math.round(wpt.getCoor().lon());
385+ byte geoid = GeoidData.getGeoid(lat, lon);
386+
387+ System.out.println(
388+ String.format("Geoid(%d, %d) = %d", lat, lon, geoid));
389+ */
390+ return 0;
391+ }
392+
393+ /**
394+ * Reduces a given list of way points to the specified target size.
395+ *
396+ * @param origList
397+ * The original list containing the way points.
398+ * @param targetSize
399+ * The desired target size of the list. The resulting list may
400+ * contain fewer items, so targetSize should be considered as
401+ * maximum.
402+ * @return A list containing the reduced list.
403+ */
404+ public static List<WayPoint> downsampleWayPoints(List<WayPoint> origList,
405+ int targetSize) {
406+ if (origList == null)
407+ return null;
408+ if (targetSize <= 0)
409+ throw new IllegalArgumentException(
410+ "targetSize must be greater than zero");
411+
412+ int origSize = origList.size();
413+ if (origSize <= targetSize) {
414+ return origList;
415+ }
416+
417+ int delta = (int) Math.max(Math.ceil(origSize / targetSize), 2);
418+
419+ List<WayPoint> res = new ArrayList<>(targetSize);
420+ for (int i = 0; i < origSize; i += delta) {
421+ res.add(origList.get(i));
422+ }
423+
424+ return res;
425+ }
426+
427+ /**
428+ * Gets the hour value of a way point in 24h format.
429+ */
430+ public static int getHourOfWayPoint(WayPoint wpt) {
431+ if (wpt == null) return -1;
432+
433+ Calendar calendar = GregorianCalendar.getInstance(); // creates a new calendar instance
434+ calendar.setTimeInMillis(wpt.getTimeInMillis()); // assigns calendar to given date
435+ return calendar.get(Calendar.HOUR_OF_DAY);
436+ }
437+
438+ /**
439+ * Gets the minute value of a way point in 24h format.
440+ */
441+ public static int getMinuteOfWayPoint(WayPoint wpt) {
442+ if (wpt == null) return -1;
443+
444+ Calendar calendar = GregorianCalendar.getInstance(); // creates a new calendar instance
445+ calendar.setTimeInMillis(wpt.getTimeInMillis()); // assigns calendar to given date
446+ return calendar.get(Calendar.MINUTE);
447+ }
448+}
449diff --git a/src/org/openstreetmap/josm/data/elevation/HgtDownloadListener.java b/src/org/openstreetmap/josm/data/elevation/HgtDownloadListener.java
450new file mode 100644
451index 000000000..c2de17a6c
452--- /dev/null
453+++ b/src/org/openstreetmap/josm/data/elevation/HgtDownloadListener.java
454@@ -0,0 +1,46 @@
455+// License: GPL. For details, see LICENSE file.
456+package org.openstreetmap.josm.data.elevation;
457+
458+import java.io.File;
459+
460+import org.openstreetmap.josm.data.coor.ILatLon;
461+
462+/**
463+ *
464+ * @author Harald Hetzner
465+ *
466+ */
467+public interface HgtDownloadListener {
468+
469+ /**
470+ * Informs the implementing class that downloading of HGT data for the given
471+ * coordinates has started.
472+ *
473+ * To be called by the thread downloading as soon as downloading actually started.
474+ *
475+ * @param latLon The coordinates for which the HGT data is now being downloaded.
476+ */
477+ public void hgtFileDownloading(ILatLon latLon);
478+
479+ /**
480+ * Informs the implementing class that HGT data for the given coordinates was
481+ * successfully downloaded.
482+ *
483+ * To be called by the thread downloading as soon as the download finished.
484+ *
485+ * @param latLon The coordinates for which the download of HGT data succeeded.
486+ * @param hgtFile The downloaded HGT file.
487+ */
488+ public void hgtFileDownloadSucceeded(ILatLon latLon, File hgtFile);
489+
490+ /**
491+ * Informs the implementing class that downloading HGT data for the given
492+ * coordinates failed.
493+ *
494+ * To be called by the thread downloading as soon as downloading fails.
495+ *
496+ * @param latLon The coordinates for which the download of HGT data failed.
497+ */
498+ public void hgtFileDownloadFailed(ILatLon latLon);
499+
500+}
501diff --git a/src/org/openstreetmap/josm/data/elevation/HgtDownloader.java b/src/org/openstreetmap/josm/data/elevation/HgtDownloader.java
502new file mode 100644
503index 000000000..880d74497
504--- /dev/null
505+++ b/src/org/openstreetmap/josm/data/elevation/HgtDownloader.java
506@@ -0,0 +1,163 @@
507+// License: GPL. For details, see LICENSE file.
508+package org.openstreetmap.josm.data.elevation;
509+
510+import java.io.File;
511+import java.io.IOException;
512+import java.io.InputStream;
513+import java.net.MalformedURLException;
514+import java.net.URL;
515+import java.nio.file.Files;
516+import java.nio.file.Path;
517+import java.nio.file.Paths;
518+import java.nio.file.StandardCopyOption;
519+import java.util.LinkedList;
520+import java.util.concurrent.ExecutorService;
521+import java.util.concurrent.Executors;
522+
523+import org.openstreetmap.josm.data.coor.ILatLon;
524+import org.openstreetmap.josm.gui.io.DownloadFileTask;
525+import org.openstreetmap.josm.spi.preferences.Config;
526+import org.openstreetmap.josm.tools.Http1Client;
527+import org.openstreetmap.josm.tools.Http1Client.Http1Response;
528+import org.openstreetmap.josm.tools.HttpClient;
529+import org.openstreetmap.josm.tools.Logging;
530+
531+/**
532+ * Class {@code HgtDownloader} downloads SRTM HGT (Shuttle Radar Topography Mission Height) files with elevation data.
533+ * Currently this class is restricted to a resolution of 3 arc seconds (SRTM3).
534+ *
535+ * SRTM3 HGT files are available at
536+ * <a href="https://e4ftl01.cr.usgs.gov/MEASURES/SRTMGL3.003/2000.02.11/">NASA's Land Processes Distributed Active Archive Center (LP DAAC)</a>.
537+ *
538+ * In order to access these files, registration at <a href="https://urs.earthdata.nasa.gov/users/new/">NASA Earthdata Login User Registration</a>
539+ * and creating an authorization bearer token on this site are required.
540+ *
541+ * @author Harald Hetzner
542+ * @see HgtReader
543+ *
544+ */
545+public class HgtDownloader {
546+
547+ private URL baseUrl;
548+ private File hgtDirectory;
549+
550+ private String authHeader;
551+
552+ private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();
553+
554+ private LinkedList<HgtDownloadListener> downloadListeners = new LinkedList<HgtDownloadListener>();
555+
556+ public HgtDownloader(File hgtDirectory, String url, String bearer) throws MalformedURLException {
557+ // May throw MalformedURLException
558+ this.baseUrl = new URL(url);
559+ // https://stackoverflow.com/questions/38085964/authorization-bearer-token-in-httpclient
560+ if (!bearer.equals(""))
561+ authHeader = "Bearer " + bearer;
562+ else
563+ authHeader = null;
564+ this.hgtDirectory = hgtDirectory;
565+ }
566+
567+ public HgtDownloader(File hgtDirectory) throws MalformedURLException {
568+ this(hgtDirectory, HgtPreferences.HGT_SERVER_BASE_URL,
569+ Config.getPref().get(HgtPreferences.ELEVATION_SERVER_AUTH_BEARER, HgtPreferences.DEFAULT_ELEVATION_SERVER_AUTH_BEARER));
570+ }
571+
572+ public void setHgtDirectory(File hgtDirectory) {
573+ this.hgtDirectory = hgtDirectory;
574+ }
575+
576+ public void addDownloadListener(HgtDownloadListener listener) {
577+ if (!downloadListeners.contains(listener))
578+ downloadListeners.add(listener);
579+ }
580+
581+ public void downloadHgtFile(ILatLon latLon) {
582+ EXECUTOR.submit(new DownloadHgtFileTask(latLon));
583+ }
584+
585+ /**
586+ * Gets the associated HGT file name for the given coordinate. Usually the
587+ * format is <tt>[N|S]nn[W|E]mmm.hgt</tt> where <i>nn</i> is the integral latitude
588+ * without decimals and <i>mmm</i> is the longitude.
589+ *
590+ * @param latLon The coordinate to get the filename for
591+ * @return The file name of the HGT file.
592+ */
593+ public static String getHgtZipFileName(ILatLon latLon) {
594+ return HgtReader.getHgtPrefix(latLon) + HgtPreferences.HGT_ZIP_FILE_PREFIX;
595+ }
596+
597+ private class DownloadHgtFileTask implements Runnable {
598+
599+ private final ILatLon latLon;
600+
601+
602+ public DownloadHgtFileTask(ILatLon latLon) {
603+ this.latLon = latLon;
604+ }
605+
606+ @Override
607+ public void run() {
608+ downloading();
609+ String hgtDirectoryPath = HgtDownloader.this.hgtDirectory.toString();
610+ String hgtZipFileName = HgtDownloader.getHgtZipFileName(latLon);
611+
612+ URL url = null;
613+ try {
614+ url = new URL(HgtDownloader.this.baseUrl + hgtZipFileName);
615+ } catch (MalformedURLException e) {
616+ downloadFailed();
617+ return;
618+ }
619+ HttpClient.setFactory(Http1Client::new);
620+ Http1Client httpClient = (Http1Client) HttpClient.create(url);
621+ if (authHeader != null)
622+ httpClient.setHeader("Authorization", authHeader);
623+ Http1Response response = null;
624+ try {
625+ response = (Http1Response) httpClient.connect();
626+ //Logging.info("Elevation: HGT server responded: " + response.getResponseCode() + " " + response.getResponseMessage());
627+ if (response.getResponseCode() != 200) {
628+ downloadFailed();
629+ return;
630+ }
631+ InputStream in = response.getInputStream();
632+ Path downloadedZipFile = Paths.get(hgtDirectoryPath, hgtZipFileName);
633+ Files.copy(in, downloadedZipFile, StandardCopyOption.REPLACE_EXISTING);
634+ DownloadFileTask.unzipFileRecursively(downloadedZipFile.toFile(), hgtDirectoryPath);
635+ Files.delete(downloadedZipFile);
636+ } catch (IOException e) {
637+ if (response != null)
638+ Logging.error("Elevation: HGT server responded: " + response.getResponseCode() + " " + response.getResponseMessage());
639+ Logging.error("Elevation: Downloading HGT file " + hgtZipFileName + " failed: " + e.toString());
640+ downloadFailed();
641+ return;
642+ }
643+ String hgtFileName = HgtReader.getHgtFileName(latLon);
644+ File downloadedHgtFile = Paths.get(hgtDirectoryPath, hgtFileName).toFile();
645+ if (!downloadedHgtFile.isFile()) {
646+ Logging.error("Downloaded HGT file " + downloadedHgtFile.toString() + " is not file !?!");
647+ downloadFailed();
648+ return;
649+ }
650+ Logging.info("Elevation: Downloaded and extracted HGT file " + hgtZipFileName + " to elevation directory: " + downloadedHgtFile.toString());
651+ downloadSucceeded(downloadedHgtFile);
652+ }
653+
654+ private void downloading() {
655+ for (HgtDownloadListener listener : HgtDownloader.this.downloadListeners)
656+ listener.hgtFileDownloading(latLon);
657+ }
658+
659+ private void downloadSucceeded(File hgtFile) {
660+ for (HgtDownloadListener listener : HgtDownloader.this.downloadListeners)
661+ listener.hgtFileDownloadSucceeded(latLon, hgtFile);
662+ }
663+
664+ private void downloadFailed() {
665+ for (HgtDownloadListener listener : HgtDownloader.this.downloadListeners)
666+ listener.hgtFileDownloadFailed(latLon);
667+ }
668+ }
669+}
670diff --git a/src/org/openstreetmap/josm/data/elevation/HgtPreferences.java b/src/org/openstreetmap/josm/data/elevation/HgtPreferences.java
671new file mode 100644
672index 000000000..6fe4854fe
673--- /dev/null
674+++ b/src/org/openstreetmap/josm/data/elevation/HgtPreferences.java
675@@ -0,0 +1,63 @@
676+// License: GPL. For details, see LICENSE file.
677+package org.openstreetmap.josm.data.elevation;
678+
679+import java.io.File;
680+import java.nio.file.Paths;
681+
682+import org.openstreetmap.josm.data.Preferences;
683+
684+/**
685+ * Property keys and default values for elevation data preferences.
686+ *
687+ * @author Harald Hetzner
688+ *
689+ */
690+public class HgtPreferences {
691+
692+ /** Property key for enabling or disabling use of elevation data. */
693+ public static final String ELEVATION_ENABLED = "elevation.enabled";
694+
695+ /** Property key for enabling or disabling automatic download of elevation data. */
696+ public static final String ELEVATION_AUTO_DOWNLOAD_ENABLED = "elevation.autodownload";
697+
698+ /**
699+ * Property key for authentication bearer token for SRTM HGT server.
700+ * @see HgtDownloader
701+ */
702+ public static final String ELEVATION_SERVER_AUTH_BEARER = "elevation.server.auth.bearer";
703+
704+ /** Default property value for enabling use of elevation data: {@code false}. */
705+ public static final boolean DEFAULT_ELEVATION_ENABLED = false;
706+
707+ /** Default property value for enabling automatic download of elevation data: {@code false}. */
708+ public static final boolean DEFAULT_ELEVATION_AUTO_DOWNLOAD_ENABLED = false;
709+
710+ /** Default property value for authentication bearer token for SRTM HGT server: Empty {@code String}. */
711+ public static final String DEFAULT_ELEVATION_SERVER_AUTH_BEARER = "";
712+
713+ /** Default path, where SRTM3 HGT files need to be placed, respectively to which they will be downloaded. */
714+ public static final File DEFAULT_HGT_DIRECTORY = Paths.get(Preferences.main().getDirs().getCacheDirectory(true).toString(), "elevation", "SRTM3").toFile();
715+
716+ /**
717+ * URL of <a href="https://urs.earthdata.nasa.gov/users/new/">NASA Earthdata Login User Registration</a>
718+ * where users need to register and create an authorization bearer token in order to download elevation
719+ * data from {@link HgtPreferences#HGT_SERVER_BASE_URL}.
720+ */
721+ public static final String HGT_SERVER_REGISTRATION_URL = "https://urs.earthdata.nasa.gov/users/new/";
722+
723+ /**
724+ * URL of
725+ * <a href="https://e4ftl01.cr.usgs.gov/MEASURES/SRTMGL3.003/2000.02.11/">NASA's Land Processes Distributed Active Archive Center (LP DAAC)</a>
726+ * where SRTM3 HGT files can be downloaded.
727+ *
728+ * Requires registration at {@link HgtPreferences#HGT_SERVER_REGISTRATION_URL}.
729+ */
730+ public static final String HGT_SERVER_BASE_URL = "https://e4ftl01.cr.usgs.gov/MEASURES/SRTMGL3.003/2000.02.11/";
731+
732+ /**
733+ * Prefix of compressed as-downloaded HGT files.
734+ */
735+ public static final String HGT_ZIP_FILE_PREFIX = ".SRTMGL3.hgt.zip";
736+
737+ private HgtPreferences() {}
738+}
739diff --git a/src/org/openstreetmap/josm/data/elevation/HgtReader.java b/src/org/openstreetmap/josm/data/elevation/HgtReader.java
740new file mode 100644
741index 000000000..a8a05efdc
742--- /dev/null
743+++ b/src/org/openstreetmap/josm/data/elevation/HgtReader.java
744@@ -0,0 +1,493 @@
745+// License: GPL. For details, see LICENSE file.
746+package org.openstreetmap.josm.data.elevation;
747+
748+import java.io.File;
749+import java.io.FileNotFoundException;
750+import java.io.IOException;
751+import java.io.InputStream;
752+import java.net.MalformedURLException;
753+import java.nio.ByteBuffer;
754+import java.nio.ByteOrder;
755+import java.nio.file.Files;
756+import java.nio.file.Paths;
757+import java.util.Arrays;
758+import java.util.HashMap;
759+import java.util.List;
760+import java.util.Optional;
761+import java.util.regex.Matcher;
762+import java.util.regex.Pattern;
763+
764+import org.apache.commons.compress.utils.IOUtils;
765+import org.openstreetmap.josm.data.Bounds;
766+import org.openstreetmap.josm.data.coor.ILatLon;
767+import org.openstreetmap.josm.io.Compression;
768+import org.openstreetmap.josm.tools.CheckParameterUtil;
769+import org.openstreetmap.josm.tools.Logging;
770+
771+/**
772+ * Class {@code HgtReader} reads elevation data from SRTM HGT (Shuttle Radar Topography Mission Height) files.
773+ * Currently this class is restricted to a resolution of 3 arc seconds (SRTM3).
774+ *
775+ * SRTM3 HGT files are available at
776+ * <a href="https://e4ftl01.cr.usgs.gov/MEASURES/SRTMGL3.003/2000.02.11/">NASA's Land Processes Distributed Active Archive Center (LP DAAC)</a>.
777+ *
778+ * In order to access these files, registration at <a href="https://urs.earthdata.nasa.gov/users/new/">NASA Earthdata Login User Registration</a>
779+ * and creating an authorization bearer token on this site are required.
780+ *
781+ * @author Oliver Wieland &lt;oliver.wieland@online.de&gt;
782+ * @author Harald Hetzner
783+ * @see HgtDownloader
784+ */
785+public class HgtReader implements HgtDownloadListener {
786+ private static final int SRTM_EXTENT = 1; // degree
787+ private static final List<String> COMPRESSION_EXT = Arrays.asList("xz", "gzip", "zip", "bz", "bz2");
788+
789+ public static final String HGT_EXT = ".hgt";
790+
791+ // alter these values for different SRTM resolutions
792+ public static final int HGT_VOID = Short.MIN_VALUE; // magic number which indicates 'void data' in HGT file
793+
794+ private static final HashMap<String, HgtCacheData> cache = new HashMap<>();
795+
796+ private static HgtReader hgtReader = null;
797+
798+ private File hgtDirectory = null;
799+ private boolean autoDownloadEnabled = false;
800+ private HgtDownloader hgtDownloader = null;
801+
802+
803+ public static HgtReader getInstance() {
804+ if (hgtReader == null)
805+ hgtReader = new HgtReader();
806+ return hgtReader;
807+ }
808+
809+ public static void destroyInstance() {
810+ hgtReader = null;
811+ }
812+
813+ private HgtReader() {
814+ this(HgtPreferences.DEFAULT_HGT_DIRECTORY);
815+ }
816+
817+ private HgtReader(File hgtDirectory) {
818+ setHgtDirectory(hgtDirectory);
819+ }
820+
821+ public void setHgtDirectory(File hgtDirectory) {
822+ if (!hgtDirectory.exists() && hgtDirectory.mkdirs())
823+ Logging.info("Elevation: Created directory for HGT files: " + hgtDirectory.toString());
824+ if (hgtDirectory.isDirectory()) {
825+ this.hgtDirectory = hgtDirectory;
826+ Logging.info("Elevation: Set directory for HGT files to: " + hgtDirectory.toString());
827+ }
828+ else {
829+ Logging.error("Elevation: Could not create directory for HGT files: " + hgtDirectory.toString());
830+ hgtDirectory = null;
831+ }
832+ if (hgtDownloader != null)
833+ hgtDownloader.setHgtDirectory(hgtDirectory);
834+ }
835+
836+ // TODO: Enable setting the URL, username and password
837+ public void setAutoDownloadEnabled(boolean enabled) {
838+ if (autoDownloadEnabled == enabled)
839+ return;
840+ if (enabled) {
841+ if (hgtDirectory != null) {
842+ if (hgtDownloader == null)
843+ try {
844+ hgtDownloader = new HgtDownloader(hgtDirectory);
845+ hgtDownloader.addDownloadListener(this);
846+ } catch (MalformedURLException e) {
847+ autoDownloadEnabled = false;
848+ Logging.error("Elevation: Cannot enable auto-downloading: " + e.toString());
849+ return;
850+ }
851+ else
852+ hgtDownloader.setHgtDirectory(hgtDirectory);
853+ autoDownloadEnabled = true;
854+ Logging.info("Elevation: Enabled auto-downloading of HGT files to " + hgtDirectory.toString());
855+ }
856+ else {
857+ hgtDownloader = null;
858+ autoDownloadEnabled = false;
859+ Logging.error("Elevation: Cannot enable auto-downloading as directory for HGT files was not set");
860+ }
861+ }
862+ else {
863+ hgtDownloader = null;
864+ autoDownloadEnabled = false;
865+ Logging.info("Elevation: Disabled auto-downloading of HGT files");
866+ }
867+ }
868+
869+ /**
870+ * Returns the elevation at the location of the provided coordinate.
871+ * If there is not HGT file with elevation data for this location and
872+ * <code>autoDownload</code> is enabled, it will be attempted to download
873+ * the HGT file.
874+ *
875+ * @param coor The location at which the elevation is of interest.
876+ * @return The elevation at the provided location or {@link ElevationHelper#NO_ELEVATION ElevationHelper.NO_ELEVATION}
877+ * if no HGT file elevation data for the location is available at present.
878+ */
879+ public double getElevationFromHgt(ILatLon coor) {
880+ File hgtDirectory = this.hgtDirectory;
881+ if (hgtDirectory == null)
882+ return ElevationHelper.NO_ELEVATION;
883+ String name = getHgtPrefix(coor);
884+ String fileName = getHgtFileName(coor);
885+ HgtCacheData hgtCacheData;
886+ try {
887+ synchronized(cache) {
888+ hgtCacheData = cache.get(name);
889+ // data not in cache
890+ if (hgtCacheData == null) {
891+ File hgtFile = Paths.get(hgtDirectory.getAbsolutePath(), fileName).toFile();
892+ // If a HGT file with the data exists locally, read it in
893+ if (hgtFile.exists()) {
894+ short[][] data = readHgtFile(hgtFile.toString());
895+ hgtCacheData = new HgtCacheData(data);
896+ cache.put(name, hgtCacheData);
897+ }
898+ // Otherwise, put an empty data set with status "missing" into the cache
899+ else {
900+ hgtCacheData = new HgtCacheData();
901+ cache.put(name, hgtCacheData);
902+ }
903+ }
904+ }
905+ // Read elevation value if HGT data is available
906+ if (hgtCacheData.getStatus() == HgtCacheData.Status.VALID)
907+ return readElevation(coor);
908+ // If the HGT file with the relevant elevation data is missing and auto-downloading is enabled, try to download it
909+ else if (hgtCacheData.getStatus() == HgtCacheData.Status.MISSING && autoDownloadEnabled)
910+ hgtDownloader.downloadHgtFile(coor);
911+ // If not valid elevation data could be returned, return no elevation
912+ return ElevationHelper.NO_ELEVATION;
913+ } catch (FileNotFoundException e) {
914+ Logging.error("Get elevation from HGT " + coor + " failed: => " + e.getMessage());
915+ // no problem... file not there
916+ return ElevationHelper.NO_ELEVATION;
917+ } catch (Exception ioe) {
918+ // oops...
919+ Logging.error(ioe);
920+ // fallback
921+ return ElevationHelper.NO_ELEVATION;
922+ }
923+ }
924+
925+ // TODO: This method is not needed to display elevation at current location in JOSM GUI
926+ public static Bounds read(File file) throws IOException {
927+ String location = file.getName();
928+ for (String ext : COMPRESSION_EXT) {
929+ location = location.replaceAll("\\." + ext + "$", "");
930+ }
931+ short[][] data = readHgtFile(file.getPath());
932+ // Overwrite the cache file (assume that is desired)
933+ HgtCacheData hgtCacheData = new HgtCacheData(data);
934+ // TODO: This method does not ensure that "location" derived from file name conforms to the naming pattern
935+ cache.put(location, hgtCacheData);
936+ Pattern pattern = Pattern.compile("([NS])(\\d{2})([EW])(\\d{3})");
937+ Matcher matcher = pattern.matcher(location);
938+ if (matcher.lookingAt()) {
939+ int lat = ("S".equals(matcher.group(1)) ? -1 : 1) * Integer.parseInt(matcher.group(2));
940+ int lon = ("W".equals(matcher.group(3)) ? -1 : 1) * Integer.parseInt(matcher.group(4));
941+ return new Bounds(lat, lon, lat + 1, lon + 1);
942+ }
943+ return null;
944+ }
945+
946+ private static short[][] readHgtFile(String file) throws IOException {
947+ CheckParameterUtil.ensureParameterNotNull(file);
948+
949+ short[][] data = null;
950+
951+ try (InputStream fis = Compression.getUncompressedFileInputStream(Paths.get(file))) {
952+ // choose the right endianness
953+ ByteBuffer bb = ByteBuffer.wrap(IOUtils.toByteArray(fis));
954+ //System.out.println(Arrays.toString(bb.array()));
955+ bb.order(ByteOrder.BIG_ENDIAN);
956+ int size = (int) Math.sqrt(bb.array().length / 2);
957+ data = new short[size][size];
958+ int x = 0;
959+ int y = 0;
960+ while (x < size) {
961+ while (y < size) {
962+ data[x][y] = bb.getShort(2 * (x * size + y));
963+ y++;
964+ }
965+ x++;
966+ y = 0;
967+ }
968+ }
969+
970+ return data;
971+ }
972+
973+ /**
974+ * Reads the elevation value for the given coordinate.
975+ *
976+ * See also <a href="http://gis.stackexchange.com/questions/43743/how-to-extract-elevation-from-hgt-file">stackexchange.com</a>
977+ * @param coor the coordinate to get the elevation data for
978+ * @return the elevation value or <code>Double.NaN</code>, if no value is present
979+ */
980+ public static double readElevation(ILatLon coor) {
981+ String fileName = getHgtFileName(coor);
982+ return readElevation(coor, fileName);
983+ }
984+
985+ /**
986+ * Reads the elevation value for the given coordinate.
987+ *
988+ * See also <a href="http://gis.stackexchange.com/questions/43743/how-to-extract-elevation-from-hgt-file">stackexchange.com</a>
989+ * @param coor the coordinate to get the elevation data for
990+ * @param fileName The expected filename
991+ * @return the elevation value or <code>Double.NaN</code>, if no value is present
992+ */
993+ public static double readElevation(ILatLon coor, String fileName) {
994+ String name = getHgtPrefix(fileName);
995+ HgtCacheData hgtCacheData;
996+
997+ synchronized(cache) {
998+ hgtCacheData = cache.get(name);
999+ }
1000+
1001+ if (hgtCacheData == null || hgtCacheData.getStatus() != HgtCacheData.Status.VALID)
1002+ return ElevationHelper.NO_ELEVATION;
1003+
1004+ short[][] data = hgtCacheData.getData();
1005+ if (data == null)
1006+ return ElevationHelper.NO_ELEVATION;
1007+
1008+ int[] index = getIndex(coor, data.length);
1009+ short ele = data[index[0]][index[1]];
1010+
1011+ if (ele == HGT_VOID)
1012+ return ElevationHelper.NO_ELEVATION;
1013+ return ele;
1014+ }
1015+
1016+ // TODO: This method is not needed to display elevation at current location in JOSM GUI
1017+ public static Optional<Bounds> getBounds(ILatLon location) {
1018+ final String fileName = getHgtFileName(location);
1019+ short[][] sb = null;
1020+
1021+ synchronized(cache) {
1022+ sb = cache.get(fileName).getData();
1023+ }
1024+
1025+ if (sb == null) {
1026+ return Optional.empty();
1027+ }
1028+
1029+ final double latDegrees = location.lat();
1030+ final double lonDegrees = location.lon();
1031+
1032+ final float fraction = ((float) SRTM_EXTENT) / sb.length;
1033+ final int latitude = (int) Math.floor(latDegrees) + (latDegrees < 0 ? 1 : 0);
1034+ final int longitude = (int) Math.floor(lonDegrees) + (lonDegrees < 0 ? 1 : 0);
1035+
1036+ final int[] index = getIndex(location, sb.length);
1037+ final int latSign = latitude > 0 ? 1 : -1;
1038+ final int lonSign = longitude > 0 ? 1 : -1;
1039+ final double minLat = latitude + latSign * fraction * index[0];
1040+ final double maxLat = latitude + latSign * fraction * (index[0] + 1);
1041+ final double minLon = longitude + lonSign * fraction * index[1];
1042+ final double maxLon = longitude + lonSign * fraction * (index[1] + 1);
1043+ return Optional.of(new Bounds(Math.min(minLat, maxLat), Math.min(minLon, maxLon),
1044+ Math.max(minLat, maxLat), Math.max(minLon, maxLon)));
1045+ }
1046+
1047+ /**
1048+ * Get the index to use for a short[latitude][longitude] = height in meters array
1049+ *
1050+ * @param latLon
1051+ * The location to get the index for
1052+ * @param mapSize
1053+ * The size of the map
1054+ * @return A [latitude, longitude] = int (index) array.
1055+ */
1056+ private static int[] getIndex(ILatLon latLon, int mapSize)
1057+ {
1058+ double latDegrees = latLon.lat();
1059+ double lonDegrees = latLon.lon();
1060+
1061+ float fraction = ((float) SRTM_EXTENT) / (mapSize - 1);
1062+ int latitude = (int) Math.round(frac(latDegrees) / fraction);
1063+ int longitude = (int) Math.round(frac(lonDegrees) / fraction);
1064+ if (latDegrees >= 0)
1065+ {
1066+ latitude = mapSize - latitude - 1;
1067+ }
1068+ if (lonDegrees < 0)
1069+ {
1070+ longitude = mapSize - longitude - 1;
1071+ }
1072+ return new int[] { latitude, longitude };
1073+ }
1074+
1075+ /**
1076+ * Gets the associated HGT file name for the given way point. Usually the
1077+ * format is <tt>[N|S]nn[W|E]mmm.hgt</tt> where <i>nn</i> is the integral latitude
1078+ * without decimals and <i>mmm</i> is the longitude.
1079+ *
1080+ * @param latLon the coordinate to get the filename for
1081+ * @return the file name of the HGT file
1082+ */
1083+ public static String getHgtFileName(ILatLon latLon) {
1084+ return getHgtPrefix(latLon) + HGT_EXT;
1085+ }
1086+
1087+ public static String getHgtPrefix(ILatLon latLon) {
1088+ int lat = (int) Math.floor(latLon.lat());
1089+ int lon = (int) Math.floor(latLon.lon());
1090+
1091+ String latPref = "N";
1092+ if (lat < 0) {
1093+ latPref = "S";
1094+ lat = Math.abs(lat);
1095+ }
1096+
1097+ String lonPref = "E";
1098+ if (lon < 0) {
1099+ lonPref = "W";
1100+ lon = Math.abs(lon);
1101+ }
1102+
1103+ return String.format("%s%2d%s%03d", latPref, lat, lonPref, lon);
1104+ }
1105+
1106+ public static String getHgtPrefix(String fileName) {
1107+ return fileName.replace(".hgt", "");
1108+ }
1109+
1110+ public static double frac(double d) {
1111+ long iPart;
1112+ double fPart;
1113+
1114+ // Get user input
1115+ iPart = (long) d;
1116+ fPart = d - iPart;
1117+ return fPart;
1118+ }
1119+
1120+ public static void clearCache() {
1121+ synchronized(cache) {
1122+ cache.clear();
1123+ }
1124+ }
1125+
1126+ @Override
1127+ public void hgtFileDownloading(ILatLon latLon) {
1128+ String name = getHgtPrefix(latLon);
1129+
1130+ synchronized (cache) {
1131+ HgtCacheData hgtCacheData = cache.get(name);
1132+ // Should not happen
1133+ if (hgtCacheData == null) {
1134+ hgtCacheData = new HgtCacheData();
1135+ cache.put(name, hgtCacheData);
1136+ }
1137+ hgtCacheData.setDownloading();
1138+ }
1139+ }
1140+
1141+ @Override
1142+ public void hgtFileDownloadSucceeded(ILatLon latLon, File hgtFile) {
1143+ String name = getHgtPrefix(latLon);
1144+ short[][] data = null;
1145+ try {
1146+ data = readHgtFile(hgtFile.getAbsolutePath());
1147+ } catch (Exception e) {
1148+ Logging.error("Elevation: Error reading HGT file " + hgtFile.getAbsolutePath() + " :" + e.toString());
1149+ }
1150+
1151+ HgtCacheData hgtCacheData;
1152+ synchronized (cache) {
1153+ hgtCacheData = cache.get(name);
1154+ if (hgtCacheData != null)
1155+ hgtCacheData.setData(data);
1156+ // Should not happen
1157+ else {
1158+ hgtCacheData = new HgtCacheData(data);
1159+ cache.put(name,hgtCacheData);
1160+ }
1161+ }
1162+
1163+ // In case that the downloaded file is corrupt, try to delete it
1164+ if (data == null) {
1165+ hgtCacheData.setDownloadFailed();
1166+ Logging.info("Elevation: Deleting downloaeded, but corrupt HGT file: " + hgtFile.getAbsolutePath());
1167+ try {
1168+ Files.delete(Paths.get(hgtFile.getAbsolutePath()));
1169+ } catch (IOException e) {
1170+ Logging.error("Elevation: Error deleting downloaded, but corrupt HGT file: " + e.toString());
1171+ }
1172+ }
1173+ }
1174+
1175+ @Override
1176+ public void hgtFileDownloadFailed(ILatLon latLon) {
1177+ String name = getHgtPrefix(latLon);
1178+
1179+ synchronized (cache) {
1180+ HgtCacheData hgtCacheData = cache.get(name);
1181+ // Should not happen
1182+ if (hgtCacheData == null) {
1183+ hgtCacheData = new HgtCacheData();
1184+ cache.put(name, hgtCacheData);
1185+ }
1186+ hgtCacheData.setDownloadFailed();
1187+ }
1188+
1189+ }
1190+
1191+ private static class HgtCacheData {
1192+
1193+ private short[][] data;
1194+ private Status status;
1195+
1196+ enum Status {
1197+ VALID,
1198+ MISSING,
1199+ DOWNLOADING,
1200+ DOWNLOAD_FAILED
1201+ }
1202+
1203+ public HgtCacheData() {
1204+ this(null);
1205+ }
1206+
1207+ public HgtCacheData(short[][] data) {
1208+ setData(data);
1209+ }
1210+
1211+ public short[][] getData() {
1212+ return data;
1213+ }
1214+
1215+ public Status getStatus() {
1216+ return status;
1217+ }
1218+
1219+ public void setDownloading() {
1220+ status = Status.DOWNLOADING;
1221+ data = null;
1222+ }
1223+
1224+ public void setDownloadFailed() {
1225+ status = Status.DOWNLOAD_FAILED;
1226+ data = null;
1227+ }
1228+
1229+ public void setData(short[][] data) {
1230+ if (data == null)
1231+ status = Status.MISSING;
1232+ else
1233+ status = Status.VALID;
1234+ this.data = data;
1235+ }
1236+ }
1237+}
1238diff --git a/src/org/openstreetmap/josm/data/elevation/gpx/GeoidCorrectionKind.java b/src/org/openstreetmap/josm/data/elevation/gpx/GeoidCorrectionKind.java
1239new file mode 100644
1240index 000000000..5b555716e
1241--- /dev/null
1242+++ b/src/org/openstreetmap/josm/data/elevation/gpx/GeoidCorrectionKind.java
1243@@ -0,0 +1,15 @@
1244+// License: GPL. For details, see LICENSE file.
1245+package org.openstreetmap.josm.data.elevation.gpx;
1246+
1247+/**
1248+ * @author Oliver Wieland &lt;oliver.wieland@online.de&gt;
1249+ * Enumeration for available elevation correction modes.
1250+ */
1251+public enum GeoidCorrectionKind {
1252+ /** Elevation values remain unchanged */
1253+ None,
1254+ /** Automatic correction by geoid lookup table */
1255+ Auto,
1256+ /** Fixed value */
1257+ Fixed
1258+}
1259diff --git a/src/org/openstreetmap/josm/gui/MapStatus.java b/src/org/openstreetmap/josm/gui/MapStatus.java
1260index 83ba6364b..2d7b01343 100644
1261--- a/src/org/openstreetmap/josm/gui/MapStatus.java
1262+++ b/src/org/openstreetmap/josm/gui/MapStatus.java
1263@@ -67,6 +67,9 @@ import org.openstreetmap.josm.data.coor.conversion.CoordinateFormatManager;
1264 import org.openstreetmap.josm.data.coor.conversion.DMSCoordinateFormat;
1265 import org.openstreetmap.josm.data.coor.conversion.ICoordinateFormat;
1266 import org.openstreetmap.josm.data.coor.conversion.ProjectedCoordinateFormat;
1267+import org.openstreetmap.josm.data.elevation.ElevationHelper;
1268+import org.openstreetmap.josm.data.elevation.HgtPreferences;
1269+import org.openstreetmap.josm.data.elevation.HgtReader;
1270 import org.openstreetmap.josm.data.osm.DataSelectionListener;
1271 import org.openstreetmap.josm.data.osm.DataSet;
1272 import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
1273@@ -124,6 +127,7 @@ public final class MapStatus extends JPanel implements
1274 Helpful, Destroyable, PreferenceChangedListener, SoMChangeListener, DataSelectionListener, DataSetListener, ZoomChangeListener {
1275
1276 private final DecimalFormat DECIMAL_FORMAT = new DecimalFormat(Config.getPref().get("statusbar.decimal-format", "0.00"));
1277+ private final DecimalFormat ELEVATION_FORMAT = new DecimalFormat("0 m");
1278 private static final AbstractProperty<Double> DISTANCE_THRESHOLD = new DoubleProperty("statusbar.distance-threshold", 0.01).cached();
1279
1280 private static final AbstractProperty<Boolean> SHOW_ID = new BooleanProperty("osm-primitives.showid", false);
1281@@ -246,6 +250,8 @@ public final class MapStatus extends JPanel implements
1282 null, DMSCoordinateFormat.INSTANCE.latToString(LatLon.SOUTH_POLE).length(), PROP_BACKGROUND_COLOR.get());
1283 private final ImageLabel lonText = new ImageLabel("lon",
1284 null, DMSCoordinateFormat.INSTANCE.lonToString(new LatLon(0, 180)).length(), PROP_BACKGROUND_COLOR.get());
1285+ private final ImageLabel eleText = new ImageLabel("ele",
1286+ tr("The terrain elevation at the mouse pointer."), 10, PROP_BACKGROUND_COLOR.get());
1287 private final ImageLabel headingText = new ImageLabel("heading",
1288 tr("The (compass) heading of the line segment being drawn."),
1289 DECIMAL_FORMAT.format(360).length() + 1, PROP_BACKGROUND_COLOR.get());
1290@@ -279,6 +285,8 @@ public final class MapStatus extends JPanel implements
1291
1292 private final transient List<StatusTextHistory> statusText = new ArrayList<>();
1293
1294+ private boolean elevationEnabled = Config.getPref().getBoolean(HgtPreferences.ELEVATION_ENABLED, HgtPreferences.DEFAULT_ELEVATION_ENABLED);
1295+
1296 protected static final class StatusTextHistory {
1297 private final Object id;
1298 private final String text;
1299@@ -912,6 +920,7 @@ public final class MapStatus extends JPanel implements
1300
1301 latText.setForeground(PROP_FOREGROUND_COLOR.get());
1302 lonText.setForeground(PROP_FOREGROUND_COLOR.get());
1303+ eleText.setForeground(PROP_FOREGROUND_COLOR.get());
1304 headingText.setForeground(PROP_FOREGROUND_COLOR.get());
1305 distText.setForeground(PROP_FOREGROUND_COLOR.get());
1306 nameText.setForeground(PROP_FOREGROUND_COLOR.get());
1307@@ -925,6 +934,8 @@ public final class MapStatus extends JPanel implements
1308 add(latText, GBC.std());
1309 add(lonText, GBC.std().insets(3, 0, 0, 0));
1310 add(headingText, GBC.std().insets(3, 0, 0, 0));
1311+ add(eleText, GBC.std().insets(3, 0, 0, 0));
1312+ eleText.setVisible(elevationEnabled);
1313 add(angleText, GBC.std().insets(3, 0, 0, 0));
1314 add(distText, GBC.std().insets(3, 0, 0, 0));
1315
1316@@ -972,6 +983,11 @@ public final class MapStatus extends JPanel implements
1317 };
1318 mv.addComponentListener(mvComponentAdapter);
1319
1320+ if (elevationEnabled) {
1321+ boolean elevationAutoDownloadEnabled = Config.getPref().getBoolean(HgtPreferences.ELEVATION_AUTO_DOWNLOAD_ENABLED, HgtPreferences.DEFAULT_ELEVATION_AUTO_DOWNLOAD_ENABLED);
1322+ HgtReader.getInstance().setAutoDownloadEnabled(elevationAutoDownloadEnabled);
1323+ }
1324+
1325 // The background thread
1326 thread = new Thread(collector, "Map Status Collector");
1327 thread.setDaemon(true);
1328@@ -998,6 +1014,16 @@ public final class MapStatus extends JPanel implements
1329 lonText.setToolTipText(tr("The geographic longitude at the mouse pointer."));
1330 previousCoordinateFormat = mCord;
1331 }
1332+ if (elevationEnabled)
1333+ updateEleText(p);
1334+ }
1335+
1336+ private void updateEleText(ILatLon coor) {
1337+ double ele = HgtReader.getInstance().getElevationFromHgt(coor);
1338+ if (ElevationHelper.isValidElevation(ele))
1339+ eleText.setText(ELEVATION_FORMAT.format(ele));
1340+ else
1341+ eleText.setText(tr("No data"));
1342 }
1343
1344 @Override
1345@@ -1299,4 +1325,14 @@ public final class MapStatus extends JPanel implements
1346 autoLength = b;
1347 }
1348
1349+ /**
1350+ * Enable or disable displaying elevation at the position of the mouse pointer.
1351+ * @param enabled If {@code true} displaying of elevation is enabled, else disabled.
1352+ * @see HgtReader
1353+ */
1354+ public void setElevationEnabled(boolean enabled) {
1355+ elevationEnabled = enabled;
1356+ eleText.setVisible(enabled);
1357+ }
1358+
1359 }
1360diff --git a/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java b/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java
1361index bb311319e..8eca8947e 100644
1362--- a/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java
1363+++ b/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java
1364@@ -53,6 +53,7 @@ import org.openstreetmap.josm.gui.preferences.display.DrawingPreference;
1365 import org.openstreetmap.josm.gui.preferences.display.GPXPreference;
1366 import org.openstreetmap.josm.gui.preferences.display.LafPreference;
1367 import org.openstreetmap.josm.gui.preferences.display.LanguagePreference;
1368+import org.openstreetmap.josm.gui.preferences.elevation.HgtPreference;
1369 import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference;
1370 import org.openstreetmap.josm.gui.preferences.map.BackupPreference;
1371 import org.openstreetmap.josm.gui.preferences.map.MapPaintPreference;
1372@@ -418,6 +419,14 @@ public final class PreferenceTabbedPane extends JTabbedPane implements ExpertMod
1373 return getSetting(ServerAccessPreference.class);
1374 }
1375
1376+ /**
1377+ * Returns the {@code HgtServerAccessPreference} object.
1378+ * @return the {@code HgtServerAccessPreference} object.
1379+ */
1380+ public HgtPreference getHgtServerPreference() {
1381+ return getSetting(HgtPreference.class);
1382+ }
1383+
1384 /**
1385 * Returns the {@code ValidatorPreference} object.
1386 * @return the {@code ValidatorPreference} object.
1387@@ -621,6 +630,7 @@ public final class PreferenceTabbedPane extends JTabbedPane implements ExpertMod
1388 SETTINGS_FACTORIES.add(new ValidatorTagCheckerRulesPreference.Factory());
1389 SETTINGS_FACTORIES.add(new RemoteControlPreference.Factory());
1390 SETTINGS_FACTORIES.add(new ImageryPreference.Factory());
1391+ SETTINGS_FACTORIES.add(new HgtPreference.Factory());
1392 }
1393
1394 /**
1395diff --git a/src/org/openstreetmap/josm/gui/preferences/elevation/HgtPreference.java b/src/org/openstreetmap/josm/gui/preferences/elevation/HgtPreference.java
1396new file mode 100644
1397index 000000000..13a392a14
1398--- /dev/null
1399+++ b/src/org/openstreetmap/josm/gui/preferences/elevation/HgtPreference.java
1400@@ -0,0 +1,80 @@
1401+// License: GPL. For details, see LICENSE file.
1402+package org.openstreetmap.josm.gui.preferences.elevation;
1403+
1404+import static org.openstreetmap.josm.tools.I18n.tr;
1405+
1406+import javax.swing.Box;
1407+
1408+import org.openstreetmap.josm.data.elevation.HgtPreferences;
1409+import org.openstreetmap.josm.data.elevation.HgtReader;
1410+import org.openstreetmap.josm.gui.MainApplication;
1411+import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting;
1412+import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
1413+import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
1414+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
1415+import org.openstreetmap.josm.spi.preferences.Config;
1416+import org.openstreetmap.josm.spi.preferences.IPreferences;
1417+import org.openstreetmap.josm.tools.GBC;
1418+
1419+/**
1420+ * Elevation data sub-preferences in preferences.
1421+ * @author Harald Hetzner
1422+ *
1423+ */
1424+public final class HgtPreference extends DefaultTabPreferenceSetting {
1425+
1426+ /**
1427+ * Factory used to create a new {@code HgtPreference}.
1428+ */
1429+ public static class Factory implements PreferenceSettingFactory {
1430+ @Override
1431+ public PreferenceSetting createPreferenceSetting() {
1432+ return new HgtPreference();
1433+ }
1434+ }
1435+
1436+ private HgtPreferencesPanel pnlHgtPreferences;
1437+
1438+ private HgtPreference() {
1439+ super(/* ICON(preferences/) */ "elevation", tr("Elevation Data"), tr("Elevation preferences and connection settings for the HGT server."));
1440+ }
1441+
1442+ @Override
1443+ public void addGui(PreferenceTabbedPane gui) {
1444+ pnlHgtPreferences = new HgtPreferencesPanel();
1445+ pnlHgtPreferences.add(Box.createVerticalGlue(), GBC.eol().fill());
1446+ gui.createPreferenceTab(this).add(pnlHgtPreferences, GBC.eol().fill());
1447+ }
1448+
1449+ /**
1450+ * Saves the values to the preferences and applies them.
1451+ */
1452+ @Override
1453+ public boolean ok() {
1454+ // Save to preferences file
1455+ pnlHgtPreferences.saveToPreferences();
1456+
1457+ // Apply preferences
1458+ IPreferences pref = Config.getPref();
1459+ boolean elevationEnabled = pref.getBoolean(HgtPreferences.ELEVATION_ENABLED, HgtPreferences.DEFAULT_ELEVATION_ENABLED);
1460+ if (elevationEnabled) {
1461+ boolean elevationAutoDownloadEnabled = pref.getBoolean(HgtPreferences.ELEVATION_AUTO_DOWNLOAD_ENABLED, HgtPreferences.DEFAULT_ELEVATION_AUTO_DOWNLOAD_ENABLED);
1462+ // If enabled, HgtDownloader used by HgtReader will by itself read the authentication bearer token from the preferences
1463+ HgtReader.getInstance().setAutoDownloadEnabled(elevationAutoDownloadEnabled);
1464+ }
1465+ else {
1466+ HgtReader.destroyInstance();
1467+ }
1468+ if (MainApplication.getMap() != null && MainApplication.getMap().statusLine != null)
1469+ MainApplication.getMap().statusLine.setElevationEnabled(elevationEnabled);
1470+
1471+ return false;
1472+ }
1473+
1474+ @Override
1475+ public String getHelpContext() {
1476+ // TODO: Add help
1477+ //return HelpUtil.ht("/Preferences/Elevation");
1478+ return null;
1479+ }
1480+}
1481diff --git a/src/org/openstreetmap/josm/gui/preferences/elevation/HgtPreferencesPanel.java b/src/org/openstreetmap/josm/gui/preferences/elevation/HgtPreferencesPanel.java
1482new file mode 100644
1483index 000000000..3f869b05e
1484--- /dev/null
1485+++ b/src/org/openstreetmap/josm/gui/preferences/elevation/HgtPreferencesPanel.java
1486@@ -0,0 +1,178 @@
1487+// License: GPL. For details, see LICENSE file.
1488+package org.openstreetmap.josm.gui.preferences.elevation;
1489+
1490+import static org.openstreetmap.josm.tools.I18n.tr;
1491+
1492+import java.awt.Desktop;
1493+import java.awt.Dimension;
1494+import java.awt.GridBagConstraints;
1495+import java.awt.GridBagLayout;
1496+import java.awt.Insets;
1497+import java.io.IOException;
1498+import java.net.URI;
1499+
1500+import javax.swing.BorderFactory;
1501+import javax.swing.JCheckBox;
1502+import javax.swing.JLabel;
1503+import javax.swing.JPanel;
1504+import javax.swing.event.HyperlinkEvent;
1505+
1506+import org.openstreetmap.josm.data.elevation.HgtPreferences;
1507+import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
1508+import org.openstreetmap.josm.gui.widgets.JosmTextField;
1509+import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
1510+import org.openstreetmap.josm.spi.preferences.Config;
1511+import org.openstreetmap.josm.spi.preferences.IPreferences;
1512+import org.openstreetmap.josm.tools.GBC;
1513+import org.openstreetmap.josm.tools.Logging;
1514+
1515+/**
1516+ * Component allowing input of HGT (elevation) server settings.
1517+ */
1518+public class HgtPreferencesPanel extends VerticallyScrollablePanel {
1519+
1520+ static final class AutoSizePanel extends JPanel {
1521+ AutoSizePanel() {
1522+ super(new GridBagLayout());
1523+ }
1524+
1525+ @Override
1526+ public Dimension getMinimumSize() {
1527+ return getPreferredSize();
1528+ }
1529+ }
1530+
1531+ private final JCheckBox cbEnableElevation = new JCheckBox(tr("Enable Use of Elevation Data"));
1532+ private final JMultilineLabel lblpElevationData =
1533+ new JMultilineLabel(tr("<html>STRM3 HGT files can be downloaded from <a href=\"{0}\">{0}</a>.</html>", HgtPreferences.HGT_SERVER_BASE_URL));
1534+ private final JCheckBox cbEnableAutoDownload = new JCheckBox(tr("Enable Automatic Downloading of Elevation Data"));
1535+ private final JLabel lblAuthBearer = new JLabel(tr("Authorization Bearer Token:"));
1536+ private final JosmTextField tfAuthBearer = new JosmTextField();
1537+ private final JMultilineLabel lblAuthBearerNotes =
1538+ new JMultilineLabel(tr("<html>You need to register at <a href=\"{0}\">{0}</a> to create the authorization bearer token.</html>",
1539+ HgtPreferences.HGT_SERVER_REGISTRATION_URL));
1540+
1541+ /**
1542+ * Builds the panel for the elevation preferences.
1543+ *
1544+ * @return Panel with elevation preferences
1545+ */
1546+ protected final JPanel buildHgtPreferencesPanel() {
1547+ cbEnableElevation.setToolTipText(tr("STRM3 HGT files need to be placed in {0}", HgtPreferences.DEFAULT_HGT_DIRECTORY.getAbsolutePath()));
1548+ cbEnableElevation.addItemListener(event -> updateEnabledState());
1549+
1550+ lblpElevationData.setEditable(false);
1551+ lblpElevationData.addHyperlinkListener(event -> browseHyperlink(event));
1552+
1553+ cbEnableAutoDownload.setToolTipText(tr("STRM3 HGT files will be downloaded from {0}", HgtPreferences.HGT_SERVER_BASE_URL));
1554+ cbEnableAutoDownload.addItemListener(event -> updateEnabledState());
1555+
1556+ lblAuthBearerNotes.setEditable(false);
1557+ lblAuthBearerNotes.addHyperlinkListener(event -> browseHyperlink(event));
1558+
1559+ JPanel pnl = new AutoSizePanel();
1560+ GridBagConstraints gc = new GridBagConstraints();
1561+
1562+ gc.anchor = GridBagConstraints.LINE_START;
1563+ gc.insets = new Insets(5, 5, 0, 0);
1564+ gc.fill = GridBagConstraints.HORIZONTAL;
1565+ gc.gridwidth = 2;
1566+ gc.weightx = 1.0;
1567+ pnl.add(cbEnableElevation, gc);
1568+
1569+ gc.gridy = 1;
1570+ pnl.add(lblpElevationData, gc);
1571+
1572+ gc.gridy = 2;
1573+ pnl.add(cbEnableAutoDownload, gc);
1574+
1575+ gc.gridy = 3;
1576+ gc.fill = GridBagConstraints.NONE;
1577+ gc.gridwidth = 1;
1578+ gc.weightx = 0.0;
1579+ pnl.add(lblAuthBearer, gc);
1580+
1581+ gc.gridx = 1;
1582+ gc.fill = GridBagConstraints.HORIZONTAL;
1583+ gc.weightx = 1.0;
1584+ pnl.add(tfAuthBearer, gc);
1585+
1586+ gc.gridy = 4;
1587+ gc.gridx = 0;
1588+ gc.gridwidth = 2;
1589+ gc.weightx = 1.0;
1590+ pnl.add(lblAuthBearerNotes, gc);
1591+
1592+ // add an extra spacer, otherwise the layout is broken
1593+ gc.gridy = 5;
1594+ gc.gridwidth = 2;
1595+ gc.fill = GridBagConstraints.BOTH;
1596+ gc.weighty = 1.0;
1597+ pnl.add(new JPanel(), gc);
1598+ return pnl;
1599+ }
1600+
1601+
1602+ /**
1603+ * Initializes the panel with the values from the preferences.
1604+ */
1605+ public final void initFromPreferences() {
1606+ IPreferences pref = Config.getPref();
1607+
1608+ cbEnableElevation.setSelected(pref.getBoolean(HgtPreferences.ELEVATION_ENABLED, HgtPreferences.DEFAULT_ELEVATION_ENABLED));
1609+ cbEnableAutoDownload.setSelected(pref.getBoolean(HgtPreferences.ELEVATION_AUTO_DOWNLOAD_ENABLED, HgtPreferences.DEFAULT_ELEVATION_AUTO_DOWNLOAD_ENABLED));
1610+ tfAuthBearer.setText(pref.get(HgtPreferences.ELEVATION_SERVER_AUTH_BEARER, HgtPreferences.DEFAULT_ELEVATION_SERVER_AUTH_BEARER));
1611+ }
1612+
1613+ private final void updateEnabledState() {
1614+ if (cbEnableElevation.isSelected()) {
1615+ lblpElevationData.setEnabled(true);
1616+ cbEnableAutoDownload.setEnabled(true);
1617+ lblAuthBearer.setEnabled(cbEnableAutoDownload.isSelected());
1618+ tfAuthBearer.setEnabled(cbEnableAutoDownload.isSelected());
1619+ lblAuthBearerNotes.setEnabled(cbEnableAutoDownload.isSelected());
1620+ }
1621+ else {
1622+ lblpElevationData.setEnabled(false);
1623+ cbEnableAutoDownload.setEnabled(false);
1624+ lblAuthBearer.setEnabled(false);
1625+ tfAuthBearer.setEnabled(false);
1626+ lblAuthBearerNotes.setEnabled(false);
1627+ }
1628+ }
1629+
1630+ // https://stackoverflow.com/questions/14101000/hyperlink-to-open-in-browser-in-java
1631+ // https://www.codejava.net/java-se/swing/how-to-create-hyperlink-with-jlabel-in-java-swing
1632+ private final void browseHyperlink(HyperlinkEvent event) {
1633+ if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
1634+ String url = event.getURL().toString();
1635+ try {
1636+ Desktop.getDesktop().browse(URI.create(url));
1637+ } catch (IOException e) {
1638+ Logging.error(e.toString());
1639+ }
1640+ }
1641+ }
1642+
1643+ /**
1644+ * Constructs a new {@code HgtPreferencesPanel}.
1645+ */
1646+ public HgtPreferencesPanel() {
1647+ setLayout(new GridBagLayout());
1648+ setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
1649+ add(buildHgtPreferencesPanel(), GBC.eop().anchor(GridBagConstraints.NORTHWEST).fill(GridBagConstraints.BOTH));
1650+
1651+ initFromPreferences();
1652+ updateEnabledState();
1653+ }
1654+
1655+ /**
1656+ * Saves the current values to the preferences
1657+ */
1658+ public void saveToPreferences() {
1659+ IPreferences pref = Config.getPref();
1660+ pref.putBoolean(HgtPreferences.ELEVATION_ENABLED, cbEnableElevation.isSelected());
1661+ pref.putBoolean(HgtPreferences.ELEVATION_AUTO_DOWNLOAD_ENABLED, cbEnableAutoDownload.isSelected());
1662+ pref.put(HgtPreferences.ELEVATION_SERVER_AUTH_BEARER, tfAuthBearer.getText());
1663+ }
1664+}