Subversion Repositories mkgmap

Rev

Rev 4586 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * Copyright (C) 2017.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */

package uk.me.parabola.mkgmap.reader.hgt;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.log.Logger;

import static java.nio.channels.FileChannel.MapMode.READ_ONLY;

/**
 * Rather simple code to read a single HGT file with SRTM data. Based on old code in class HGTDEM
 * in package uk.me.parabola.mkgmap.reader.dem which was removed in 2017.
 * @author Gerd Petermann
 *
 */

public class HGTReader {
        private static final Logger log = Logger.getLogger(HGTReader.class);

        public static final short UNDEF = Short.MIN_VALUE;
        private ByteBuffer buffer;
        private int res;
        private final String fileName;
        private String path;
        private boolean read;
        private long count;
        private int numPixelsX;
        private int numPixelsY;

       
        private static final Map<String,Set<String>> missingMap = new HashMap<>();
        private static final Set<String> badDir = new HashSet<>();
       
        /**
         * Class to read a single HGT file.
         * @param lat in degrees, -90 .. 90
         * @param lon - -180..180
         * @param dirsWithHGT string with comma separated list of directories to search for *.hgt files
         * Supported are also zip files containing *.hgt files and directories containing *.hgt.zip.
         */

        public HGTReader(int lat, int lon, String dirsWithHGT) {
                String baseName = String.format("%s%02d%s%03d",
                                lat < 0 ? "S" : "N", lat < 0 ? -lat : lat,
                                                lon < 0 ? "W" : "E", lon < 0 ? -lon : lon);
               
                String[] dirs = dirsWithHGT.split("[,]");
                fileName = baseName + ".hgt";
                String fName = ".";
                boolean knownAsMissing = false;
                synchronized (missingMap) {
                        Set<String> missingSet = missingMap.get(dirsWithHGT);
                        if (missingSet != null)
                                knownAsMissing = missingSet.contains(fileName);
                }
                if (!knownAsMissing) {
                        for (String dir : dirs) {
                                dir = dir.trim();
                                File f = new File (dir);
                                if (!f.exists()) {
                                        synchronized (badDir) {
                                                if (badDir.add(dir))
                                                        log.error("extracted path >" + dir + "< does not exist, check option dem:", dirsWithHGT);
                                        }
                                        continue;
                                }
                                if (f.isDirectory()) {
                                        fName = Utils.joinPath(dir, fileName);
                                        try (FileInputStream fis = new FileInputStream(fName)) {
                                                res = calcRes(fis.getChannel().size(), fName);
                                                if (res >= 0) {
                                                        path = fName;
                                                }
                                                break;
                                        } catch (FileNotFoundException e) {
                                        } catch (IOException e) {
                                                log.error("failed to get size for file", fName);
                                        }
                                        fName += ".zip";
                                        checkZip(fName, fileName); // try to find *.hgt.zip in dir that contains *.hgt
                                        if (res > 0) {
                                                path = fName;
                                                return;
                                        }
                                        fName = Utils.joinPath(dir, baseName) + ".zip";
                                        checkZip(fName, fileName); // try to find *.hgt.zip in dir that contains *.hgt
                                        if (res > 0) {
                                                path = fName;
                                                return;
                                        }
                                } else {
                                        if (dir.endsWith(".zip")) {
                                                checkZip(dir, fileName); // try to find *.hgt in zip file
                                        }
                                }
                                if (res > 0) {
                                        path = dir;
                                        return;
                                }
                        }
                        if (res <= 0 || path == null) {
                                res = -1;
                                path = null;
                                synchronized (missingMap){
                                        missingMap.computeIfAbsent(dirsWithHGT, k-> new HashSet<>()).add(fileName);
                                }
                                HGTList hgtList = HGTList.get();
                                if (hgtList != null) {
                                        if (hgtList.shouldExist(lat, lon))
                                                Logger.defaultLogger.warn(this.getClass().getSimpleName() + ": file " + fileName + " not found but it should exist. Height values will be 0.");
                                } else {
                                        log.warn("file " + fileName + " not found. Is expected to cover sea.");
                                }
                        }
                }
        }
       
        /**
         * try to find the needed file. Different hgt providers use slightly different methods to
         * pack their data.
         * @param zipFile
         * @param name
         * @return
         */

        private ZipEntry findZipEntry (ZipFile zipFile, String name) {
                ZipEntry entry = zipFile.getEntry(name);
                if (entry == null) {
                        // no direct hit, try to recurse through all files
                        Enumeration<? extends ZipEntry> entries = zipFile.entries();
                        while (entries.hasMoreElements()) {
                                entry = entries.nextElement();
                                if (!entry.isDirectory() && entry.getName().toUpperCase().endsWith(name.toUpperCase())) {
                                        return entry;
                                }
                        }
                        return null;
                }
                return entry;
        }
       
        /**
         * Check if we can find the wanted file in a zip container and if it has the right size.
         * @param fName path to container
         * @param name wanted file
         */

        private void checkZip(String fName, String name) {
                File f = new File(fName);
                if (!f.exists())
                        return;
                try(ZipFile zipFile = new ZipFile(f)){
                        ZipEntry entry = findZipEntry(zipFile, name);
                        if (entry != null){
                                res = calcRes(entry.getSize(), entry.getName());
                        }
                } catch (IOException exp) {
                        log.error("failed to get size for file", name, "from", fName);
                }
        }

        /**
         * Try to unzip the file contained in a zip file.
         * @param zipFile
         * @param entry
         * @throws IOException
         */

        private void extractFromZip(String fName, String name) throws IOException {
                try (ZipFile zipFile = new ZipFile(fName)) {
                        ZipEntry entry = findZipEntry(zipFile, name);
                        if (entry != null) {
                                InputStream is = zipFile.getInputStream(entry);
                                log.info("extracting data for " + entry.getName() + " from " + zipFile.getName());
                                buffer = ByteBuffer.allocate((int) entry.getSize());
                                byte[] ioBuffer = new byte[1024];
                                int len = is.read(ioBuffer);
                                while (len != -1) {
                                        buffer.put(ioBuffer, 0, len);
                                        len = is.read(ioBuffer);
                                }
                        } else {
                                throw new FileNotFoundException(name);
                        }
                        read = true;
                }
        }

        /**
         * calculate the resolution of the hgt file. size should be exactly 2 * (res+1) * (res+1)
         * @param size number of bytes
         * @param fname file name (for error possible message)
         * @return resolution (typically 1200 for 3'' or 3600 for 1'')
         */

        private int calcRes(long size, String fname) {
                long numVals = (long) Math.sqrt(size / 2d);
                if (2 * 1801 * 3601 == size) {
                        numPixelsX = 1801;
                        numPixelsY = 3601;
                        return 3600;
                }
                if (2 * numVals * numVals == size) {
                        numPixelsX = (int) numVals;
                        numPixelsY = (int) numVals;
                        return (int) (numVals - 1);
                }
                log.error("file", fname, "has unexpected size", size, "and is ignored");
                return -1;
        }

        /**
         * HGT files are organised as a matrix of n*n (short) values giving the elevation in metres.
         * Invalid values are coded as 0x8000 = -327678 = Short.MIN_VALUE.
         * @param x index for column west to east
         * @param y index for row north to south
         * @return the elevation value stored in the file or 0 if
         */

        public short ele(int x, int y) {
                if (!read && path != null) {
                        prepRead();
                }
                if (buffer == null)
                        return 0;
                assert (x >= 0 && x < numPixelsX && y >= 0 && y < numPixelsY) :
                        "wrong x/y value for res" + numPixelsX + "x" + numPixelsY + " x=" + x + " y=" + y;
                count++;
                return buffer.getShort(2 * ((numPixelsY - 1 - y) * numPixelsX + x));
               
        }

        /**
         * @return the resolution to use with this file, -1 is return if file is invalid
         */

        public int getRes() {
                return res;
        }
       
        @Override
        public String toString() {
                return fileName + " (" + count + " reads) " + res ;
        }

        /**
         * Return memory to GC.
         * @return true if heap memory was freed.
         */

        public boolean freeBuf() {
                if (buffer == null)
                        return false;
                buffer = null;
                read = false;
                return true;
        }

        public void prepRead() {
                if (!read && path != null) {
                        try {
                                if (count == 0)
                                        log.info("allocating buffer for", fileName);
                                else
                                        log.warn("re-allocating buffer for", fileName);
                                if (path.endsWith(".zip"))
                                        extractFromZip(path, fileName);
                                else {
                                        try (FileInputStream is = new FileInputStream(path)) {
                                                buffer = is.getChannel().map(READ_ONLY, 0, is.getChannel().size());
                                                read = true;
                                        }
                                }
                        } catch (FileNotFoundException e) {
                                throw new MapFailedException("previously existing file is missing: " + path);
                        } catch (IOException e) {
                                log.error("failed to create buffer for file", path);
                        }

                }
        }

        public int getResX() {
                return numPixelsX - 1;
        }

        public int getResY() {
                return numPixelsY - 1;
        }
       
}