/*
* Licensed to GraphHopper GmbH under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*
* GraphHopper GmbH licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.graphhopper.reader.dem;
import com.graphhopper.coll.GHIntObjectHashMap;
import com.graphhopper.storage.DAType;
import com.graphhopper.storage.DataAccess;
import com.graphhopper.storage.Directory;
import com.graphhopper.storage.GHDirectory;
import com.graphhopper.util.BitUtil;
import com.graphhopper.util.Downloader;
import com.graphhopper.util.Helper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.SocketTimeoutException;
import java.util.zip.ZipInputStream;
/**
* Elevation data from NASA (SRTM).
* <p>
* Important information about SRTM: the coordinates of the lower-left corner of tile N40W118 are 40
* degrees north latitude and 118 degrees west longitude. To be more exact, these coordinates refer
* to the geometric center of the lower left sample, which in the case of SRTM3 data will be about
* 90 meters in extent.
* <p>
*
* @author Peter Karich
*/
public class SRTMProvider implements ElevationProvider {
private static final BitUtil BIT_UTIL = BitUtil.BIG;
private final Logger logger = LoggerFactory.getLogger(getClass());
private final int DEFAULT_WIDTH = 1201;
private final int WIDTH_BYTE_INDEX = 0;
// use a map as an array is not quite useful if we want to hold only parts of the world
private final GHIntObjectHashMap<HeightTile> cacheData = new GHIntObjectHashMap<HeightTile>();
private final GHIntObjectHashMap<String> areas = new GHIntObjectHashMap<String>();
private final double precision = 1e7;
private final double invPrecision = 1 / precision;
private Directory dir;
private DAType daType = DAType.MMAP;
private Downloader downloader = new Downloader("GraphHopper SRTMReader").setTimeout(10000);
private File cacheDir = new File("/tmp/srtm");
// possible alternatives see #451
// http://mirror.ufs.ac.za/datasets/SRTM3/
//"http://dds.cr.usgs.gov/srtm/version2_1/SRTM3/"
private String baseUrl = "https://srtm.kurviger.de/SRTM3/";
private boolean calcMean = false;
public SRTMProvider() {
// move to explicit calls?
init();
}
public static void main(String[] args) throws IOException {
SRTMProvider provider = new SRTMProvider();
// 1046
System.out.println(provider.getEle(47.468668, 14.575127));
// 1113
System.out.println(provider.getEle(47.467753, 14.573911));
// 1946
System.out.println(provider.getEle(46.468835, 12.578777));
// 845
System.out.println(provider.getEle(48.469123, 9.576393));
// 1113 vs new:
provider.setCalcMean(true);
System.out.println(provider.getEle(47.467753, 14.573911));
}
@Override
public void setCalcMean(boolean calcMean) {
this.calcMean = calcMean;
}
/**
* The URLs are a bit ugly and so we need to find out which area name a certain lat,lon
* coordinate has.
*/
private SRTMProvider init() {
try {
String strs[] = {"Africa", "Australia", "Eurasia", "Islands", "North_America", "South_America"};
for (String str : strs) {
InputStream is = getClass().getResourceAsStream(str + "_names.txt");
for (String line : Helper.readFile(new InputStreamReader(is, Helper.UTF_CS))) {
int lat = Integer.parseInt(line.substring(1, 3));
if (line.substring(0, 1).charAt(0) == 'S')
lat = -lat;
int lon = Integer.parseInt(line.substring(4, 7));
if (line.substring(3, 4).charAt(0) == 'W')
lon = -lon;
int intKey = calcIntKey(lat, lon);
String key = areas.put(intKey, str);
if (key != null)
throw new IllegalStateException("do not overwrite existing! key " + intKey + " " + key + " vs. " + str);
}
}
return this;
} catch (Exception ex) {
throw new IllegalStateException("Cannot load area names from classpath", ex);
}
}
// use int key instead of string for lower memory usage
private int calcIntKey(double lat, double lon) {
// we could use LinearKeyAlgo but this is simpler as we only need integer precision:
return (down(lat) + 90) * 1000 + down(lon) + 180;
}
public void setDownloader(Downloader downloader) {
this.downloader = downloader;
}
@Override
public ElevationProvider setCacheDir(File cacheDir) {
if (cacheDir.exists() && !cacheDir.isDirectory())
throw new IllegalArgumentException("Cache path has to be a directory");
try {
this.cacheDir = cacheDir.getCanonicalFile();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return this;
}
@Override
public ElevationProvider setBaseURL(String baseUrl) {
if (baseUrl == null || baseUrl.isEmpty())
throw new IllegalArgumentException("baseUrl cannot be empty");
this.baseUrl = baseUrl;
return this;
}
@Override
public ElevationProvider setDAType(DAType daType) {
this.daType = daType;
return this;
}
int down(double val) {
int intVal = (int) val;
if (val >= 0 || intVal - val < invPrecision)
return intVal;
return intVal - 1;
}
String getFileString(double lat, double lon) {
int intKey = calcIntKey(lat, lon);
String str = areas.get(intKey);
if (str == null)
return null;
int minLat = Math.abs(down(lat));
int minLon = Math.abs(down(lon));
str += "/";
if (lat >= 0)
str += "N";
else
str += "S";
if (minLat < 10)
str += "0";
str += minLat;
if (lon >= 0)
str += "E";
else
str += "W";
if (minLon < 10)
str += "0";
if (minLon < 100)
str += "0";
str += minLon;
return str;
}
@Override
public double getEle(double lat, double lon) {
lat = (int) (lat * precision) / precision;
lon = (int) (lon * precision) / precision;
int intKey = calcIntKey(lat, lon);
HeightTile demProvider = cacheData.get(intKey);
if (demProvider != null)
return demProvider.getHeight(lat, lon);
if (!cacheDir.exists())
cacheDir.mkdirs();
String fileDetails = getFileString(lat, lon);
if (fileDetails == null)
return 0;
DataAccess heights = getDirectory().find("dem" + intKey);
boolean loadExisting = false;
try {
loadExisting = heights.loadExisting();
} catch (Exception ex) {
logger.warn("cannot load dem" + intKey + ", error:" + ex.getMessage());
}
if (!loadExisting)
updateHeightsFromZipFile(fileDetails, heights);
int width = (int) (Math.sqrt(heights.getHeader(WIDTH_BYTE_INDEX)) + 0.5);
if (width == 0)
width = DEFAULT_WIDTH;
demProvider = new HeightTile(down(lat), down(lon), width, precision, 1);
cacheData.put(intKey, demProvider);
demProvider.setCalcMean(calcMean);
demProvider.setHeights(heights);
return demProvider.getHeight(lat, lon);
}
private void updateHeightsFromZipFile(String fileDetails, DataAccess heights) throws RuntimeException {
try {
byte[] bytes = getByteArrayFromZipFile(fileDetails);
heights.create(bytes.length);
for (int bytePos = 0; bytePos < bytes.length; bytePos += 2) {
short val = BIT_UTIL.toShort(bytes, bytePos);
if (val < -1000 || val > 12000)
val = Short.MIN_VALUE;
heights.setShort(bytePos, val);
}
heights.setHeader(WIDTH_BYTE_INDEX, bytes.length / 2);
heights.flush();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private byte[] getByteArrayFromZipFile(String fileDetails) throws InterruptedException, FileNotFoundException, IOException {
String zippedURL = baseUrl + "/" + fileDetails + ".hgt.zip";
File file = new File(cacheDir, new File(zippedURL).getName());
InputStream is;
// get zip file if not already in cacheDir
if (!file.exists())
for (int i = 0; i < 3; i++) {
try {
downloader.downloadFile(zippedURL, file.getAbsolutePath());
break;
} catch (SocketTimeoutException ex) {
// just try again after a little nap
Thread.sleep(2000);
continue;
}
}
is = new FileInputStream(file);
ZipInputStream zis = new ZipInputStream(is);
zis.getNextEntry();
BufferedInputStream buff = new BufferedInputStream(zis);
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte[] buffer = new byte[0xFFFF];
int len;
while ((len = buff.read(buffer)) > 0) {
os.write(buffer, 0, len);
}
os.flush();
Helper.close(buff);
return os.toByteArray();
}
@Override
public void release() {
cacheData.clear();
// for memory mapped type we create temporary unpacked files which should be removed
if (dir != null)
dir.clear();
}
@Override
public String toString() {
return "SRTM";
}
private Directory getDirectory() {
if (dir != null)
return dir;
logger.info(this.toString() + " Elevation Provider, from: " + baseUrl + ", to: " + cacheDir + ", as: " + daType);
return dir = new GHDirectory(cacheDir.getAbsolutePath(), daType);
}
}