/* * 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(); } }