de.fhg.igd.mapviewer.server.file.FileTiler.java Source code

Java tutorial

Introduction

Here is the source code for de.fhg.igd.mapviewer.server.file.FileTiler.java

Source

/*
 * Copyright (c) 2016 Fraunhofer IGD
 * 
 * All rights reserved. This program and the accompanying materials are made
 * available under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution. If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     Fraunhofer IGD <http://www.igd.fraunhofer.de/>
 */
package de.fhg.igd.mapviewer.server.file;

import java.awt.Dimension;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.prefs.Preferences;

import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileFilter;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * FileTiler - creates map files
 *
 * @author <a href="mailto:simon.templer@igd.fhg.de">Simon Templer</a>
 *
 * @version $Id$
 */
public class FileTiler {

    /**
     * A file filter that accepts file names that contain a certain string
     */
    public static class ContainsFileFilter extends FileFilter {

        private final String contains;

        /**
         * Creates a file filter that accepts file names that contain the given
         * string
         * 
         * @param contains the string that must be contained in accepted file
         *            names
         */
        public ContainsFileFilter(String contains) {
            this.contains = contains;
        }

        /**
         * @see FileFilter#accept(File)
         */
        @Override
        public boolean accept(File f) {
            if (f.isDirectory())
                return true;
            else
                return f.getName().indexOf(contains) >= 0;
        }

        /**
         * @see FileFilter#getDescription()
         */
        @Override
        public String getDescription() {
            return contains + " file";
        }

    }

    /**
     * A file filter that accepts a certain file name
     */
    public static class ExactFileFilter extends FileFilter {

        private final String fileName;

        /**
         * Creates a file filter that accepts the given file name
         * 
         * @param fileName the file name
         */
        public ExactFileFilter(String fileName) {
            this.fileName = fileName;
        }

        /**
         * @see FileFilter#accept(java.io.File)
         */
        @Override
        public boolean accept(File f) {
            if (f.isDirectory())
                return true;
            else
                return f.getName().equals(fileName);
        }

        /**
         * @see FileFilter#getDescription()
         */
        @Override
        public String getDescription() {
            return fileName;
        }

    }

    private static final Log log = LogFactory.getLog(FileTiler.class);

    // preferences keys
    private static final String PREF_DIR = "dir";
    private static final String PREF_CONVERT = "convert";
    private static final String PREF_IDENTIFY = "identify";

    // default values
    private static final int DEF_MIN_TILE_SIZE = 200;
    private static final int DEF_MIN_MAP_SIZE = 600;

    // map properties keys
    /** tile width (pixel) property name */
    public static final String PROP_TILE_WIDTH = "tileWidth";
    /** tile height (pixel) property name */
    public static final String PROP_TILE_HEIGHT = "tileHeight";
    /** number of zoom levels property name */
    public static final String PROP_ZOOM_LEVELS = "zoomLevels";
    /** map width (tiles) property name */
    public static final String PROP_MAP_WIDTH = "mapWidthAtZoom";
    /** map height (tiles) property name */
    public static final String PROP_MAP_HEIGHT = "mapHeightAtZoom";

    /** map properties file name */
    public static final String MAP_PROPERTIES_FILE = "map.properties";
    /** map file file-extension */
    public static final String MAP_ARCHIVE_EXTENSION = ".map";
    /** converter properties file name */
    public static final String CONVERTER_PROPERTIES_FILE = "converter.properties";

    // tile file
    /** tile file name prefix */
    public static final String TILE_FILE_PREFIX = "tile_z";
    /** tile file name separator */
    public static final String TILE_FILE_SEPARATOR = "_n";
    /** tile file file-extension */
    public static final String TILE_FILE_EXTENSION = ".jpg";

    /**
     * Buffer size for writing files into jar archive
     */
    public static int BUFFER_SIZE = 10240;

    /**
     * FileTiler preferences node
     */
    private final Preferences pref = Preferences.userNodeForPackage(FileTiler.class)
            .node(FileTiler.class.getSimpleName());

    /**
     * Path to convert executable
     */
    private String convertPath;

    /**
     * Path to identify executable
     */
    private String identifyPath;

    /**
     * Get the path to the convert executable
     * 
     * @return the path to the convert executable
     */
    private String getConvertPath() {
        if (convertPath == null)
            loadCommandPaths();

        return convertPath;
    }

    /**
     * Get the path to the identify executable
     * 
     * @return the path to the convert executable
     */
    private String getIdentifyPath() {
        if (identifyPath == null)
            loadCommandPaths();

        return identifyPath;
    }

    /**
     * Load the command paths from the preferences or ask the user for them
     */
    private void loadCommandPaths() {
        String convert = pref.get(PREF_CONVERT, null);
        String identify = pref.get(PREF_IDENTIFY, null);

        JFileChooser commandChooser = new JFileChooser();

        if (convert != null && identify != null) {
            if (JOptionPane.showConfirmDialog(null,
                    "<html>Found paths to executables:<br/><b>" + convert + "<br/>" + identify
                            + "</b><br/>Do you want to use this settings?</html>",
                    "Paths to executables", JOptionPane.YES_NO_OPTION,
                    JOptionPane.QUESTION_MESSAGE) == JOptionPane.NO_OPTION) {
                convert = null;
                identify = null;
            }
        }

        if (convert == null) {
            // ask for convert path
            convert = askForPath(commandChooser, new ContainsFileFilter("convert"),
                    "Please select your convert executable");
        }

        if (convert != null && identify == null) {
            // ask for identify path
            identify = askForPath(commandChooser, new ContainsFileFilter("identify"),
                    "Please select your identify executable");
        }

        if (convert == null)
            pref.remove(PREF_CONVERT);
        else
            pref.put(PREF_CONVERT, convert);

        if (identify == null)
            pref.remove(PREF_IDENTIFY);
        else
            pref.put(PREF_IDENTIFY, identify);

        convertPath = convert;
        identifyPath = identify;
    }

    /**
     * Ask the user for a certain file path
     * 
     * @param chooser the {@link JFileChooser} to use
     * @param filter the file filter
     * @param title the title of the dialog
     * 
     * @return the selected file or null
     */
    private String askForPath(JFileChooser chooser, FileFilter filter, String title) {
        chooser.setDialogTitle(title);
        chooser.setFileFilter(filter);
        int returnVal = chooser.showOpenDialog(null);
        if (returnVal == JFileChooser.APPROVE_OPTION) {
            return chooser.getSelectedFile().getAbsolutePath();
        }

        return null;
    }

    /**
     * Uses a Runtime.exec() to use imagemagick to perform the given conversion
     * operation. Returns true on success, false on failure. Does not check if
     * either file exists.
     *
     * @param in Description of the Parameter
     * @param out Description of the Parameter
     * @param width the new width
     * @param height the new height
     * @param quality Description of the Parameter
     * @return Description of the Return Value
     */
    public boolean convert(File in, File out, int width, int height, int quality) {
        if (quality < 0 || quality > 100) {
            quality = 75;
        }

        // note: CONVERT_PROG is a class variable that stores the location of
        // ImageMagick's convert command
        // it might be something like "/usr/local/magick/bin/convert" or
        // something else, depending on where you installed it.
        String[] command = { getConvertPath(), "-geometry", width + "x" + height, "-quality",
                String.valueOf(quality), in.getAbsolutePath(), out.getAbsolutePath() };

        return exec(command, null);
    }

    /**
     * Split an image file into tiles
     * 
     * @param in the image file
     * @param outPattern the name pattern for the tiles (e.g. tiles_%d)
     * @param extension the file extension for the tile image files
     * @param tileWidth the desired tile width
     * @param tileHeight the desired tile height
     * 
     * @return if the operation succeded
     */
    public boolean tile(File in, String outPattern, String extension, int tileWidth, int tileHeight) {
        File dir = in.getParentFile();

        String[] command = { getConvertPath(), in.getAbsolutePath(), "-crop", tileWidth + "x" + tileHeight,
                "+repage", dir.getAbsolutePath() + File.separator + outPattern + extension };

        return exec(command, null);
    }

    /**
     * Get the size of an image using the identify command
     * 
     * @param imageFile the image file
     * 
     * @return the dimension stating the size of the image or null
     */
    public Dimension getSize(File imageFile) {
        String[] command = { getIdentifyPath(), imageFile.getAbsolutePath() };

        List<String> result = new ArrayList<String>();
        boolean success = exec(command, result);

        if (success && !result.isEmpty()) {
            try {
                String[] split = result.get(0).split(" ");
                String name = imageFile.getName();
                log.info("Filename: " + name);
                boolean found = false;
                int fileIndex;
                for (fileIndex = 0; !found && fileIndex < split.length; fileIndex++) {
                    if (split[fileIndex].endsWith(name))
                        found = true;
                }

                if (found) {
                    String geometry = split[fileIndex + 1]; // get geometry part
                    String[] geosplit = geometry.split("x");
                    return new Dimension(Integer.parseInt(geosplit[0]), Integer.parseInt(geosplit[1]));
                } else
                    throw new IllegalArgumentException();
            } catch (Exception e) {
                log.error("Error getting size info for file " + imageFile.getAbsolutePath() + ", output was: "
                        + result);
            }
        }

        return null;
    }

    /**
     * Tries to exec the command, waits for it to finsih, logs errors if exit
     * status is nonzero, and returns true if exit status is 0 (success).
     *
     * @param command Description of the Parameter
     * @param output a list that will be cleared and the output lines added (if
     *            the list is not null)
     * @return Description of the Return Value
     */
    public static boolean exec(String[] command, List<String> output) {
        Process proc;

        try {
            // System.out.println("Trying to execute command " +
            // Arrays.asList(command));
            proc = Runtime.getRuntime().exec(command);
        } catch (IOException e) {
            log.error("IOException while trying to execute " + Arrays.toString(command), e);
            return false;
        }

        if (output == null)
            output = new ArrayList<String>();

        output.clear();
        BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()));

        String currentLine;
        try {
            while ((currentLine = reader.readLine()) != null) {
                output.add(currentLine);
            }
        } catch (IOException e) {
            log.error("Error reading process output", e);
        } finally {
            try {
                reader.close();
            } catch (IOException e) {
                log.error("Error closing input stream", e);
            }
        }

        int exitStatus;

        while (true) {
            try {
                exitStatus = proc.waitFor();
                break;
            } catch (java.lang.InterruptedException e) {
                log.warn("Interrupted: Ignoring and waiting");
            }
        }

        if (exitStatus != 0) {
            /*
             * StringBuilder cmdString = new StringBuilder(); for (int i = 0; i
             * < command.length; i++) cmdString.append(command[i]);
             */
            log.warn("Error executing command: " + exitStatus + " (" + output + ")");
        }

        return (exitStatus == 0);
    }

    /**
     * Ask the user for an image file for that a tiled map shall be created
     */
    public void run() {
        JFileChooser fileChooser = new JFileChooser();

        // load current dir
        fileChooser.setCurrentDirectory(
                new File(pref.get(PREF_DIR, fileChooser.getCurrentDirectory().getAbsolutePath())));

        // open
        int returnVal = fileChooser.showOpenDialog(null);
        if (returnVal == JFileChooser.APPROVE_OPTION) {
            // save current dir
            pref.put(PREF_DIR, fileChooser.getCurrentDirectory().getAbsolutePath());

            // get file
            File imageFile = fileChooser.getSelectedFile();

            // get image dimension
            Dimension size = getSize(imageFile);
            log.info("Image size: " + size);

            // ask for min tile size
            int minTileSize = 0;
            while (minTileSize <= 0) {
                try {
                    minTileSize = Integer.parseInt(
                            JOptionPane.showInputDialog("Minimal tile size", String.valueOf(DEF_MIN_TILE_SIZE)));
                } catch (Exception e) {
                    minTileSize = 0;
                }
            }

            // determine min map width
            int width = size.width;

            while (width / 2 > minTileSize && width % 2 == 0) {
                width = width / 2;
            }
            int minMapWidth = width; // min map width

            log.info("Minimal map width: " + minMapWidth);

            // determine min map height
            int height = size.height;

            while (height / 2 > minTileSize && height % 2 == 0) {
                height = height / 2; // min map height
            }
            int minMapHeight = height;

            log.info("Minimal map height: " + minMapHeight);

            // ask for min map size
            int minMapSize = 0;
            while (minMapSize <= 0) {
                try {
                    minMapSize = Integer.parseInt(
                            JOptionPane.showInputDialog("Minimal map size", String.valueOf(DEF_MIN_MAP_SIZE)));
                } catch (Exception e) {
                    minMapSize = 0;
                }
            }

            // determine zoom levels
            int zoomLevels = 1;

            width = size.width;
            height = size.height;

            while (width % 2 == 0 && height % 2 == 0 && width / 2 >= Math.max(minMapWidth, minMapSize)
                    && height / 2 >= Math.max(minMapHeight, minMapSize)) {
                zoomLevels++;
                width = width / 2;
                height = height / 2;
            }

            log.info("Number of zoom levels: " + zoomLevels);

            // determine tile width
            width = minMapWidth;
            int tileWidth = minMapWidth;
            for (int i = 3; i < Math.sqrt(minMapWidth) && width > minTileSize;) {
                tileWidth = width;
                if (width % i == 0) {
                    width = width / i;
                } else
                    i++;
            }

            // determine tile height
            height = minMapHeight;
            int tileHeight = minMapHeight;
            for (int i = 3; i < Math.sqrt(minMapHeight) && height > minTileSize;) {
                tileHeight = height;
                if (height % i == 0) {
                    height = height / i;
                } else
                    i++;
            }

            // create tiles for each zoom level
            if (JOptionPane.showConfirmDialog(null,
                    "Create tiles (" + tileWidth + "x" + tileHeight + ") for " + zoomLevels + " zoom levels?",
                    "Create tiles", JOptionPane.YES_NO_OPTION,
                    JOptionPane.QUESTION_MESSAGE) == JOptionPane.YES_OPTION) {
                int currentWidth = size.width;
                int currentHeight = size.height;
                File currentImage = imageFile;

                Properties properties = new Properties();
                properties.setProperty(PROP_TILE_WIDTH, String.valueOf(tileWidth));
                properties.setProperty(PROP_TILE_HEIGHT, String.valueOf(tileHeight));
                properties.setProperty(PROP_ZOOM_LEVELS, String.valueOf(zoomLevels));

                List<File> files = new ArrayList<File>();

                for (int i = 0; i < zoomLevels; i++) {
                    int mapWidth = currentWidth / tileWidth;
                    int mapHeight = currentHeight / tileHeight;

                    log.info("Creating tiles for zoom level " + i);
                    log.info("Map width: " + currentWidth + " pixels, " + mapWidth + " tiles");
                    log.info("Map height: " + currentHeight + " pixels, " + mapHeight + " tiles");

                    // create tiles
                    tile(currentImage, TILE_FILE_PREFIX + i + TILE_FILE_SEPARATOR + "%d", TILE_FILE_EXTENSION,
                            tileWidth, tileHeight);

                    // add files to list
                    for (int num = 0; num < mapWidth * mapHeight; num++) {
                        files.add(new File(imageFile.getParentFile().getAbsolutePath() + File.separator
                                + TILE_FILE_PREFIX + i + TILE_FILE_SEPARATOR + num + TILE_FILE_EXTENSION));
                    }

                    // store map width and height at current zoom
                    properties.setProperty(PROP_MAP_WIDTH + i, String.valueOf(mapWidth));
                    properties.setProperty(PROP_MAP_HEIGHT + i, String.valueOf(mapHeight));

                    // create image for next zoom level
                    currentWidth /= 2;
                    currentHeight /= 2;
                    // create temp image file name
                    File nextImage = suffixFile(imageFile, i + 1);
                    // resize image
                    convert(currentImage, nextImage, currentWidth, currentHeight, 100);
                    // delete previous temp file
                    if (!currentImage.equals(imageFile)) {
                        if (!currentImage.delete()) {
                            log.warn("Error deleting " + imageFile.getAbsolutePath());
                        }
                    }

                    currentImage = nextImage;
                }

                // delete previous temp file
                if (!currentImage.equals(imageFile)) {
                    if (!currentImage.delete()) {
                        log.warn("Error deleting " + imageFile.getAbsolutePath());
                    }
                }

                // write properties file
                File propertiesFile = new File(
                        imageFile.getParentFile().getAbsolutePath() + File.separator + MAP_PROPERTIES_FILE);
                try {
                    FileWriter propertiesWriter = new FileWriter(propertiesFile);
                    try {
                        properties.store(propertiesWriter, "Map generated from " + imageFile.getName());
                        // add properties file to list
                        files.add(propertiesFile);
                    } finally {
                        propertiesWriter.close();
                    }
                } catch (IOException e) {
                    log.error("Error writing map properties file", e);
                }

                // add a converter properties file
                String convProperties = askForPath(fileChooser, new ExactFileFilter(CONVERTER_PROPERTIES_FILE),
                        "Select a converter properties file");
                File convFile = null;
                if (convProperties != null) {
                    convFile = new File(convProperties);
                    files.add(convFile);
                }

                // create jar file
                log.info("Creating jar archive...");
                if (createJarArchive(replaceExtension(imageFile, MAP_ARCHIVE_EXTENSION), files)) {
                    log.info("Archive successfully created, deleting tiles...");
                    // don't delete converter properties
                    if (convFile != null)
                        files.remove(files.size() - 1);
                    // delete files
                    for (File file : files) {
                        if (!file.delete()) {
                            log.warn("Error deleting " + file.getAbsolutePath());
                        }
                    }
                }

                log.info("Fin.");
            }
        }
    }

    /**
     * Inserts an integer suffix after the file name of the given file, just
     * before the extension and returns the file object with the modified file
     * name
     * 
     * @param file the base file name
     * @param suffix the suffix that shall be inserted
     * 
     * @return the file object with the modified file name
     */
    private File suffixFile(File file, int suffix) {
        String fileName = file.getAbsolutePath();
        int index = fileName.lastIndexOf('.');
        String newName = fileName.substring(0, index) + String.valueOf(suffix) + fileName.substring(index);
        return new File(newName);
    }

    /**
     * Returns a file object that equals the given file except for the file
     * extension
     * 
     * @param file the file
     * @param extension the new extension (with leading dot)
     * 
     * @return the file object with the modified file name
     */
    private File replaceExtension(File file, String extension) {
        String fileName = file.getAbsolutePath();
        int index = fileName.lastIndexOf('.');
        String newName = fileName.substring(0, index) + extension;
        return new File(newName);
    }

    /**
     * Creates a Jar archive that includes the given list of files
     * 
     * @param archiveFile the name of the jar archive file
     * @param tobeJared the files to be included in the jar file
     * 
     * @return if the operation was successful
     */
    public static boolean createJarArchive(File archiveFile, List<File> tobeJared) {
        try {
            byte buffer[] = new byte[BUFFER_SIZE];
            // Open archive file
            FileOutputStream stream = new FileOutputStream(archiveFile);
            JarOutputStream out = new JarOutputStream(stream, new Manifest());

            for (int i = 0; i < tobeJared.size(); i++) {
                if (tobeJared.get(i) == null || !tobeJared.get(i).exists() || tobeJared.get(i).isDirectory())
                    continue; // Just in case...
                log.debug("Adding " + tobeJared.get(i).getName());

                // Add archive entry
                JarEntry jarAdd = new JarEntry(tobeJared.get(i).getName());
                jarAdd.setTime(tobeJared.get(i).lastModified());
                out.putNextEntry(jarAdd);

                // Write file to archive
                FileInputStream in = new FileInputStream(tobeJared.get(i));
                while (true) {
                    int nRead = in.read(buffer, 0, buffer.length);
                    if (nRead <= 0)
                        break;
                    out.write(buffer, 0, nRead);
                }
                in.close();
            }

            out.close();
            stream.close();
            log.info("Adding completed OK");
            return true;
        } catch (Exception e) {
            log.error("Creating jar file failed", e);
            return false;
        }
    }

    /**
     * Executes a FileTiler instance
     * 
     * @param args ignored
     */
    public static void main(String[] args) {
        new FileTiler().run();
    }

}