package net.sourceforge.gpstools.dem;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
public class HgtFile implements ElevationModel {
private static final int BUFFSIZE = 0x1000;
private static final ByteOrder BYTEORDER = ByteOrder.BIG_ENDIAN;
static final Pattern fileNamePattern = Pattern
.compile("^([SN])(\\d{2})([WE])(\\d{3})\\.[hH][gG][tT](?:\\.[zZ][iI][pP])?$");
private final ShortBuffer buffer;
private final FileChannel channel;
private final int minimumLatitude;
private final int minimumLongitude;
private final int linearResolution;
private final URI sourceURI;
private final File file;
public HgtFile(URI hgtFile, File cacheDirectory) throws IOException {
String scheme = hgtFile.getScheme();
this.sourceURI = hgtFile;
// make sure the file exists
if ("file".equals(scheme)) {
this.file = new File(hgtFile);
} else {
this.file = download(hgtFile.toURL(), cacheDirectory);
}
if (!this.file.isFile()) {
throw new FileNotFoundException(this.file.getAbsolutePath());
}
// parse the file name
Matcher m = fileNamePattern.matcher(this.file.getName());
if (m.matches()) {
int lat = Integer.parseInt(m.group(2));
if (m.group(1).charAt(0) == 'S') {
lat = -lat;
}
int lon = Integer.parseInt(m.group(4));
if (m.group(3).charAt(0) == 'W') {
lon = -lon;
}
this.minimumLatitude = lat;
this.minimumLongitude = lon;
} else {
throw new IllegalArgumentException("File name " + file.getName()
+ " cannot be parsed.");
}
// parse the file size
double linRes = Math.sqrt(this.file.length() * 8 / Short.SIZE);
this.linearResolution = (int) linRes;
if (linRes != linearResolution) {
throw new IOException(
"Hgt file is not a square array of 16 bit integers.");
}
this.channel = (new FileInputStream(this.file)).getChannel();
try {
ByteBuffer buff = channel.map(MapMode.READ_ONLY, 0, file.length());
buff.order(BYTEORDER);
this.buffer = buff.asShortBuffer();
} catch (IOException ex) {
this.channel.close();
throw ex;
}
}
private File download(URL hgtFile, File cacheDirectory) throws IOException {
String filename = hgtFile.getFile();
int j = filename.lastIndexOf('/');
if (j != -1) {
filename = filename.substring(j + 1);
}
boolean isZipped = filename.endsWith(".zip");
final File downloadedFile = new File(cacheDirectory, filename);
final File cachedFile;
final File zippedFile;
if (isZipped)
{
cachedFile = new File(cacheDirectory, filename.substring(0, filename.length() - 4));
zippedFile = downloadedFile;
}
else
{
cachedFile = downloadedFile;
zippedFile = null;
}
if (cachedFile.isFile())
{
return cachedFile;
}
else if (isZipped && zippedFile.isFile())
{
return unzip(zippedFile);
}
else
{
InputStream source = hgtFile.openStream();
try {
CopyBuffered(source, downloadedFile);
} finally {
source.close();
}
return isZipped ? unzip(downloadedFile) : downloadedFile;
}
}
private static void CopyBuffered(InputStream source, File target)
throws IOException {
File tmp = File.createTempFile(target.getName(), "tmp");
try {
FileOutputStream sink = new FileOutputStream(tmp);
try {
BufferedInputStream bufferedSource = new BufferedInputStream(
source, BUFFSIZE);
BufferedOutputStream bufferedSink = new BufferedOutputStream(
sink, BUFFSIZE);
byte[] buffer = new byte[4096];
for (int bytesRead = bufferedSource.read(buffer); bytesRead != -1; bytesRead = bufferedSource
.read(buffer)) {
bufferedSink.write(buffer, 0, bytesRead);
}
bufferedSink.flush();
} finally {
sink.close();
}
tmp.renameTo(target);
} finally {
if (tmp.exists()) {
tmp.delete();
}
}
}
private static File unzip(File zippedFile) throws ZipException, IOException {
File result;
ZipFile zip = new ZipFile(zippedFile);
try {
for (ZipEntry entry : Collections.list(zip.entries())) {
if (fileNamePattern.matcher(entry.getName()).matches()) {
InputStream source = zip.getInputStream(entry);
try {
result = new File(zippedFile.getParentFile(),
entry.getName());
CopyBuffered(source, result);
return result;
} finally {
source.close();
}
}
}
throw new FileNotFoundException("No suitable entry found in "
+ zippedFile);
} finally {
zip.close();
}
}
@Override
public void close() throws IOException {
if (this.channel != null) {
this.channel.close();
}
}
@Override
public BigDecimal getElevation(BigDecimal lat, BigDecimal lon)
throws DEMException {
float reducedLat = lat.floatValue() - this.minimumLatitude;
float reducedLon = lon.floatValue() - this.minimumLongitude;
if (reducedLat > 1 || reducedLat < 0) {
throw new DEMException(lat, lon, "Latitude outside domain.");
}
if (reducedLon > 1 || reducedLon < 0) {
throw new DEMException(lat, lon, "Longitude outside domain.");
}
final int linRes = this.linearResolution;
final int maxIndex = linRes - 1;
int columnIndex = Math.round(maxIndex * reducedLon);
int rowIndex = maxIndex - Math.round(maxIndex * reducedLat);
int offset = rowIndex * linRes + columnIndex;
buffer.position(offset);
short result = this.buffer.get();
return (result == Short.MIN_VALUE) ? null : new BigDecimal(result);
}
@Override
public String getInfo() {
return this.sourceURI.toString() + " cached at " + this.file.getAbsolutePath();
}
}