/*
* 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.storage.DAType;
import com.graphhopper.storage.DataAccess;
import com.graphhopper.storage.Directory;
import com.graphhopper.storage.GHDirectory;
import com.graphhopper.util.Downloader;
import com.graphhopper.util.Helper;
import org.apache.xmlgraphics.image.codec.tiff.TIFFDecodeParam;
import org.apache.xmlgraphics.image.codec.tiff.TIFFImageDecoder;
import org.apache.xmlgraphics.image.codec.util.SeekableStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.image.Raster;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketTimeoutException;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Elevation data from CGIAR project http://srtm.csi.cgiar.org/ 'PROCESSED SRTM DATA VERSION 4.1'.
* Every file covers a region of 5x5 degree. License granted for all people using GraphHopper:
* http://graphhopper.com/public/license/CGIAR.txt
* <p>
* Every zip contains readme.txt with the necessary information e.g.:
* <ol>
* <li>
* All GeoTiffs with 6000 x 6000 pixels.
* </li>
* </ol>
* <p>
*
* @author NopMap
* @author Peter Karich
*/
public class CGIARProvider implements ElevationProvider {
private static final int WIDTH = 6000;
final double precision = 1e7;
private final Logger logger = LoggerFactory.getLogger(getClass());
private final Map<String, HeightTile> cacheData = new HashMap<String, HeightTile>();
private final double invPrecision = 1 / precision;
private final int degree = 5;
private Downloader downloader = new Downloader("GraphHopper CGIARReader").setTimeout(10000);
private File cacheDir = new File("/tmp/cgiar");
// for alternatives see #346
private String baseUrl = "http://srtm.csi.cgiar.org/SRT-ZIP/SRTM_V41/SRTM_Data_GeoTiff";
private Directory dir;
private DAType daType = DAType.MMAP;
private boolean calcMean = false;
private boolean autoRemoveTemporary = true;
private long sleep = 2000;
public static void main(String[] args) {
CGIARProvider provider = new CGIARProvider();
System.out.println(provider.getEle(46, -20));
// 337.0
System.out.println(provider.getEle(49.949784, 11.57517));
// 453.0
System.out.println(provider.getEle(49.968668, 11.575127));
// 447.0
System.out.println(provider.getEle(49.968682, 11.574842));
// 3131
System.out.println(provider.getEle(-22.532854, -65.110474));
// 123
System.out.println(provider.getEle(38.065392, -87.099609));
// 1615
System.out.println(provider.getEle(40, -105.2277023));
System.out.println(provider.getEle(39.99999999, -105.2277023));
System.out.println(provider.getEle(39.9999999, -105.2277023));
// 1617
System.out.println(provider.getEle(39.999999, -105.2277023));
// 0
System.out.println(provider.getEle(29.840644, -42.890625));
}
@Override
public void setCalcMean(boolean eleCalcMean) {
calcMean = eleCalcMean;
}
void setSleep(long sleep) {
this.sleep = sleep;
}
/**
* Creating temporary files can take a long time as we need to unpack tiff as well as to fill
* our DataAccess object, so this option can be used to disable the default clear mechanism via
* specifying 'false'.
*/
public void setAutoRemoveTemporaryFiles(boolean autoRemoveTemporary) {
this.autoRemoveTemporary = autoRemoveTemporary;
}
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;
}
protected File getCacheDir() {
return cacheDir;
}
@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;
}
@Override
public double getEle(double lat, double lon) {
// no data we can avoid the trouble
if (lat > 60 || lat < -60)
return 0;
lat = (int) (lat * precision) / precision;
lon = (int) (lon * precision) / precision;
String name = getFileName(lat, lon);
HeightTile demProvider = cacheData.get(name);
if (demProvider == null) {
if (!cacheDir.exists())
cacheDir.mkdirs();
int minLat = down(lat);
int minLon = down(lon);
// less restrictive against boundary checking
demProvider = new HeightTile(minLat, minLon, WIDTH, degree * precision, degree);
demProvider.setCalcMean(calcMean);
cacheData.put(name, demProvider);
DataAccess heights = getDirectory().find(name + ".gh");
demProvider.setHeights(heights);
boolean loadExisting = false;
try {
loadExisting = heights.loadExisting();
} catch (Exception ex) {
logger.warn("cannot load " + name + ", error: " + ex.getMessage());
}
if (!loadExisting) {
String tifName = name + ".tif";
String zippedURL = baseUrl + "/" + name + ".zip";
File file = new File(cacheDir, new File(zippedURL).getName());
// get zip file if not already in cacheDir - unzip later and in-memory only!
if (!file.exists()) {
try {
int max = 3;
for (int trial = 0; trial < max; trial++) {
try {
downloader.downloadFile(zippedURL, file.getAbsolutePath());
break;
} catch (SocketTimeoutException ex) {
// just try again after a little nap
Thread.sleep(sleep);
if (trial >= max - 1)
throw ex;
continue;
} catch (IOException ex) {
demProvider.setSeaLevel(true);
// use small size on disc and in-memory
heights.setSegmentSize(100).create(10).
flush();
return 0;
}
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
// short == 2 bytes
heights.create(2 * WIDTH * WIDTH);
// logger.info("start decoding");
// decode tiff data
Raster raster;
SeekableStream ss = null;
try {
InputStream is = new FileInputStream(file);
ZipInputStream zis = new ZipInputStream(is);
// find tif file in zip
ZipEntry entry = zis.getNextEntry();
while (entry != null && !entry.getName().equals(tifName)) {
entry = zis.getNextEntry();
}
ss = SeekableStream.wrapInputStream(zis, true);
TIFFImageDecoder imageDecoder = new TIFFImageDecoder(ss, new TIFFDecodeParam());
raster = imageDecoder.decodeAsRaster();
} catch (Exception e) {
throw new RuntimeException("Can't decode " + tifName, e);
} finally {
if (ss != null)
Helper.close(ss);
}
// logger.info("start converting to our format");
final int height = raster.getHeight();
final int width = raster.getWidth();
int x = 0, y = 0;
try {
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++) {
short val = (short) raster.getPixel(x, y, (int[]) null)[0];
if (val < -1000 || val > 12000)
val = Short.MIN_VALUE;
heights.setShort(2 * (y * WIDTH + x), val);
}
}
heights.flush();
// TODO remove tifName and zip?
} catch (Exception ex) {
throw new RuntimeException("Problem at x:" + x + ", y:" + y, ex);
}
} // loadExisting
}
if (demProvider.isSeaLevel())
return 0;
return demProvider.getHeight(lat, lon);
}
int down(double val) {
// 'rounding' to closest 5
int intVal = (int) (val / degree) * degree;
if (!(val >= 0 || intVal - val < invPrecision))
intVal = intVal - degree;
return intVal;
}
protected String getFileName(double lat, double lon) {
lon = 1 + (180 + lon) / degree;
int lonInt = (int) lon;
lat = 1 + (60 - lat) / degree;
int latInt = (int) lat;
if (Math.abs(latInt - lat) < invPrecision / degree)
latInt--;
// replace String.format as it seems to be slow
// String.format("srtm_%02d_%02d", lonInt, latInt);
String str = "srtm_";
str += lonInt < 10 ? "0" : "";
str += lonInt;
str += latInt < 10 ? "_0" : "_";
str += latInt;
return str;
}
@Override
public void release() {
cacheData.clear();
// for memory mapped type we create temporary unpacked files which should be removed
if (autoRemoveTemporary && dir != null)
dir.clear();
}
@Override
public String toString() {
return "CGIAR";
}
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);
}
}