/******************************************************************************* * Copyright (c) MOBAC developers * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 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. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package mobac.utilities; import java.awt.Color; import java.awt.Graphics2D; import java.awt.event.ActionEvent; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.io.Writer; import java.net.HttpURLConnection; import java.net.URISyntaxException; import java.net.URL; import java.nio.channels.FileChannel; import java.security.CodeSource; import java.security.ProtectionDomain; import java.sql.SQLException; import java.sql.Statement; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.ParseException; import java.text.ParsePosition; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Properties; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.imageio.ImageIO; import javax.swing.Action; import javax.swing.ImageIcon; import javax.swing.JComponent; import mobac.Main; import mobac.exceptions.MOBACOutOfMemoryException; import mobac.program.Logging; import mobac.program.interfaces.MapSource; import mobac.program.model.TileImageType; import mobac.utilities.file.DirectoryFileFilter; import org.apache.log4j.Logger; public class Utilities { public static final Color COLOR_TRANSPARENT = new Color(0, 0, 0, 0); public static final DecimalFormatSymbols DFS_ENG = new DecimalFormatSymbols(Locale.ENGLISH); public static final DecimalFormatSymbols DFS_LOCAL = new DecimalFormatSymbols(); public static final DecimalFormat FORMAT_6_DEC = new DecimalFormat("#0.######"); public static final DecimalFormat FORMAT_6_DEC_ENG = new DecimalFormat("#0.######", DFS_ENG); public static final DecimalFormat FORMAT_2_DEC = new DecimalFormat("0.00"); private static final DecimalFormat cDmsMinuteFormatter = new DecimalFormat("00"); private static final DecimalFormat cDmsSecondFormatter = new DecimalFormat("00.0"); private static final Logger log = Logger.getLogger(Utilities.class); public static final long SECONDS_PER_HOUR = TimeUnit.HOURS.toSeconds(1); public static final long SECONDS_PER_DAY = TimeUnit.DAYS.toSeconds(1); public static boolean testJaiColorQuantizerAvailable() { try { Class<?> c = Class.forName("javax.media.jai.operator.ColorQuantizerDescriptor"); if (c != null) return true; } catch (NoClassDefFoundError e) { return false; } catch (Throwable t) { log.error("Error in testJaiColorQuantizerAvailable():", t); return false; } return true; } public static BufferedImage createEmptyTileImage(MapSource mapSource) { int tileSize = mapSource.getMapSpace().getTileSize(); Color color = mapSource.getBackgroundColor(); int imageType; if (color.getAlpha() == 255) imageType = BufferedImage.TYPE_INT_RGB; else imageType = BufferedImage.TYPE_INT_ARGB; BufferedImage emptyImage = new BufferedImage(tileSize, tileSize, imageType); Graphics2D g = emptyImage.createGraphics(); try { g.setColor(color); g.fillRect(0, 0, tileSize, tileSize); } finally { g.dispose(); } return emptyImage; } public static BufferedImage safeCreateBufferedImage(int width, int height, int imageType) { try { return new BufferedImage(width, height, imageType); } catch (OutOfMemoryError e) { int bytesPerPixel = getBytesPerPixel(imageType); if (bytesPerPixel < 0) throw e; long requiredMemory = ((long) width) * ((long) height) * bytesPerPixel; String message = String.format( "Available free memory not sufficient for creating image of size %dx%d pixels", width, height); throw new MOBACOutOfMemoryException(requiredMemory, message); } } /** * * @param imageType * as used for {@link BufferedImage#BufferedImage(int, int, int)} * @return */ public static int getBytesPerPixel(int bufferedImageType) { switch (bufferedImageType) { case BufferedImage.TYPE_INT_ARGB: case BufferedImage.TYPE_INT_ARGB_PRE: case BufferedImage.TYPE_INT_BGR: case BufferedImage.TYPE_4BYTE_ABGR: case BufferedImage.TYPE_4BYTE_ABGR_PRE: return 4; case BufferedImage.TYPE_3BYTE_BGR: return 3; case BufferedImage.TYPE_USHORT_GRAY: case BufferedImage.TYPE_USHORT_565_RGB: case BufferedImage.TYPE_USHORT_555_RGB: return 2; case BufferedImage.TYPE_BYTE_GRAY: case BufferedImage.TYPE_BYTE_BINARY: case BufferedImage.TYPE_BYTE_INDEXED: return 1; } return -1; } public static byte[] createEmptyTileData(MapSource mapSource) { BufferedImage emptyImage = createEmptyTileImage(mapSource); ByteArrayOutputStream buf = new ByteArrayOutputStream(4096); try { ImageIO.write(emptyImage, mapSource.getTileImageType().getFileExt(), buf); } catch (IOException e) { throw new RuntimeException(e); } byte[] emptyTileData = buf.toByteArray(); return emptyTileData; } private static final byte[] PNG = new byte[] { (byte) 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A }; private static final byte[] JPG = new byte[] { (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE0, (byte) 0x00, 0x10, 'J', 'F', 'I', 'F' }; private static final byte[] GIF_1 = "GIF87a".getBytes(); private static final byte[] GIF_2 = "GIF89a".getBytes(); public static TileImageType getImageType(byte[] imageData) { if (imageData == null) return null; if (startsWith(imageData, PNG)) return TileImageType.PNG; if (startsWith(imageData, JPG)) return TileImageType.JPG; if (startsWith(imageData, GIF_1) || startsWith(imageData, GIF_2)) return TileImageType.GIF; return null; } public static boolean startsWith(byte[] data, byte[] startTest) { if (data.length < startTest.length) return false; for (int i = 0; i < startTest.length; i++) if (data[i] != startTest[i]) return false; return true; } /** * Checks if the available JAXB version is at least v2.1 */ public static boolean checkJAXBVersion() { try { // We are trying to load the class javax.xml.bind.JAXB which has // been introduced with JAXB 2.1. Previous version do not contain // this class and will therefore throw an exception. Class<?> c = Class.forName("javax.xml.bind.JAXB"); return (c != null); } catch (ClassNotFoundException e) { return false; } } public static InputStream loadResourceAsStream(String resourcePath) throws IOException { return Main.class.getResourceAsStream("resources/" + resourcePath); } public static String loadTextResource(String resourcePath) throws IOException { DataInputStream in = new DataInputStream(Main.class.getResourceAsStream("resources/" + resourcePath)); byte[] buf; buf = new byte[in.available()]; in.readFully(buf); in.close(); String text = new String(buf, Charsets.UTF_8); return text; } /** * * @param imageName * imagePath resource path relative to the class {@link Main} * @return */ public static ImageIcon loadResourceImageIcon(String imageName) { URL url = Main.class.getResource("resources/images/" + imageName); return new ImageIcon(url); } public static URL getResourceImageUrl(String imageName) { return Main.class.getResource("resources/images/" + imageName); } public static void loadProperties(Properties p, URL url) throws IOException { InputStream propIn = url.openStream(); try { p.load(propIn); } finally { closeStream(propIn); } } public static void loadProperties(Properties p, File f) throws IOException { InputStream propIn = new FileInputStream(f); try { p.load(propIn); } finally { closeStream(propIn); } } public static void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { throw new RuntimeException(e); } } /** * Checks if the current {@link Thread} has been interrupted and if so a {@link InterruptedException}. Therefore it * behaves similar to {@link Thread#sleep(long)} without actually slowing down anything by sleeping a certain amount * of time. * * @throws InterruptedException */ public static void checkForInterruption() throws InterruptedException { if (Thread.currentThread().isInterrupted()) throw new InterruptedException(); } /** * Checks if the current {@link Thread} has been interrupted and if so a {@link RuntimeException} will be thrown. * This method is useful for long lasting operations that do not allow to throw an {@link InterruptedException}. * * @throws RuntimeException */ public static void checkForInterruptionRt() throws RuntimeException { if (Thread.currentThread().isInterrupted()) throw new RuntimeException(new InterruptedException()); } public static void closeFile(RandomAccessFile file) { if (file == null) return; try { file.close(); } catch (IOException e) { } } public static void closeStream(InputStream in) { if (in == null) return; try { in.close(); } catch (IOException e) { } } public static void close(Closeable c) { if (c == null) return; try { c.close(); } catch (IOException e) { } } public static void closeStream(OutputStream out) { if (out == null) return; try { out.close(); } catch (IOException e) { } } public static void closeWriter(Writer writer) { if (writer == null) return; try { writer.close(); } catch (IOException e) { } } public static void closeReader(OutputStream reader) { if (reader == null) return; try { reader.close(); } catch (IOException e) { } } public static void closeStatement(Statement statement) { if (statement == null) return; try { statement.close(); } catch (SQLException e) { } } public static double parseLocaleDouble(String text) throws ParseException { ParsePosition pos = new ParsePosition(0); Number n = Utilities.FORMAT_6_DEC.parse(text, pos); if (n == null) throw new ParseException("Unknown error", 0); if (pos.getIndex() != text.length()) throw new ParseException("Text ends with unparsable characters", pos.getIndex()); return n.doubleValue(); } public static void showTooltipNow(JComponent c) { Action toolTipAction = c.getActionMap().get("postTip"); if (toolTipAction != null) { ActionEvent postTip = new ActionEvent(c, ActionEvent.ACTION_PERFORMED, ""); toolTipAction.actionPerformed(postTip); } } /** * Formats a byte value depending on the size to "Bytes", "KiBytes", "MiByte" and "GiByte" * * @param bytes * @return Formatted {@link String} */ public static String formatBytes(long bytes) { if (bytes < 1000) return Long.toString(bytes) + " " + I18nUtils.localizedStringForKey("Bytes"); if (bytes < 1000000) return FORMAT_2_DEC.format(bytes / 1024d) + " " + I18nUtils.localizedStringForKey("KiByte"); if (bytes < 1000000000) return FORMAT_2_DEC.format(bytes / 1048576d) + " " + I18nUtils.localizedStringForKey("MiByte"); return FORMAT_2_DEC.format(bytes / 1073741824d) + " " + I18nUtils.localizedStringForKey("GiByte"); } public static String formatDurationSeconds(long seconds) { long x = seconds; long days = x / SECONDS_PER_DAY; x %= SECONDS_PER_DAY; int years = (int) (days / 365); days -= (years * 365); int months = (int) (days * 12d / 365d); String m = (months == 1) ? "month" : "months"; if (years > 5) return String.format("%d years", years); if (years > 0) { String y = (years == 1) ? "year" : "years"; return String.format("%d %s %d %s", years, y, months, m); } String d = (days == 1) ? "day" : "days"; if (months > 0) { days -= months * (365d / 12d); return String.format("%d %s %d %s", months, m, days, d); } long hours = TimeUnit.SECONDS.toHours(x); String h = (hours == 1) ? "hour" : "hours"; x -= hours * SECONDS_PER_HOUR; if (days > 0) return String.format("%d %s %d %s", days, d, hours, h); long minutes = TimeUnit.SECONDS.toMinutes(x); String min = (minutes == 1) ? "minute" : "minutes"; if (hours > 0) return String.format("%d %s %d %s", hours, h, minutes, min); else return String.format("%d %s", minutes, min); } public static void mkDir(File dir) throws IOException { if (dir.isDirectory()) return; if (!dir.mkdir()) throw new IOException("Failed to create directory \"" + dir.getAbsolutePath() + "\""); } public static void mkDirs(File dir) throws IOException { if (dir.isDirectory()) return; if (dir.mkdirs()) return; if (Logging.isCONFIGURED()) Logging.LOG.error("mkDirs creation failed first time - one retry left"); // Wait some time and then retry it. // See for details: // http://javabyexample.wisdomplug.com/component/content/article/37-core-java/48-is-mkdirs-thread-safe.html // Hopefully this will fix the different bugs reported for this method try { Thread.sleep(100); } catch (InterruptedException e) { } if (dir.mkdirs()) return; throw new IOException("Failed to create directory \"" + dir.getAbsolutePath() + "\""); } public static void fileCopy(File sourceFile, File destFile) throws IOException { FileChannel source = null; FileChannel destination = null; FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream(sourceFile); fos = new FileOutputStream(destFile); source = fis.getChannel(); destination = fos.getChannel(); destination.transferFrom(source, 0, source.size()); } finally { fis.close(); fos.close(); if (source != null) { source.close(); } if (destination != null) { destination.close(); } } } public static byte[] getFileBytes(File file) throws IOException { int size = (int) file.length(); byte[] buffer = new byte[size]; DataInputStream in = new DataInputStream(new FileInputStream(file)); try { in.readFully(buffer); return buffer; } finally { closeStream(in); } } /** * Fully reads data from <tt>in</tt> to an internal buffer until the end of in has been reached. Then the buffer is * returned. * * @param in * data source to be read * @return buffer all data available in in * @throws IOException */ public static byte[] getInputBytes(InputStream in) throws IOException { int initialBufferSize = in.available(); if (initialBufferSize <= 0) initialBufferSize = 32768; ByteArrayOutputStream buffer = new ByteArrayOutputStream(initialBufferSize); byte[] b = new byte[1024]; int ret = 0; while ((ret = in.read(b)) >= 0) { buffer.write(b, 0, ret); } return buffer.toByteArray(); } /** * Fully reads data from <tt>in</tt> the read data is discarded. * * @param in * @throws IOException */ public static void readFully(InputStream in) throws IOException { byte[] b = new byte[4096]; while ((in.read(b)) >= 0) { } } /** * Lists all direct sub directories of <code>dir</code> * * @param dir * @return list of directories */ public static File[] listSubDirectories(File dir) { return dir.listFiles(new DirectoryFileFilter()); } public static List<File> listSubDirectoriesRec(File dir, int maxDepth) { List<File> dirList = new LinkedList<File>(); addSubDirectories(dirList, dir, maxDepth); return dirList; } public static void addSubDirectories(List<File> dirList, File dir, int maxDepth) { File[] subDirs = dir.listFiles(new DirectoryFileFilter()); for (File f : subDirs) { dirList.add(f); if (maxDepth > 0) addSubDirectories(dirList, f, maxDepth - 1); } } public static String prettyPrintLatLon(double coord, boolean isCoordKindLat) { boolean neg = coord < 0.0; String c; if (isCoordKindLat) { c = (neg ? "S" : "N"); } else { c = (neg ? "W" : "E"); } double tAbsCoord = Math.abs(coord); int tDegree = (int) tAbsCoord; double tTmpMinutes = (tAbsCoord - tDegree) * 60; int tMinutes = (int) tTmpMinutes; double tSeconds = (tTmpMinutes - tMinutes) * 60; return c + tDegree + "\u00B0" + cDmsMinuteFormatter.format(tMinutes) + "\'" + cDmsSecondFormatter.format(tSeconds) + "\""; } public static void setHttpProxyHost(String host) { if (host != null && host.length() > 0) System.setProperty("http.proxyHost", host); else System.getProperties().remove("http.proxyHost"); } public static void setHttpProxyPort(String port) { if (port != null && port.length() > 0) System.setProperty("http.proxyPort", port); else System.getProperties().remove("http.proxyPort"); } /** * Returns the file path for the selected class. If the class is located inside a JAR file the return value contains * the directory that contains the JAR file. If the class file is executed outside of an JAR the root directory * holding the class/package structure is returned. * * @param mainClass * @return * @throws URISyntaxException */ public static File getClassLocation(Class<?> mainClass) { ProtectionDomain pDomain = mainClass.getProtectionDomain(); CodeSource cSource = pDomain.getCodeSource(); File f; try { URL loc = cSource.getLocation(); // file:/c:/almanac14/examples/ f = new File(loc.toURI()); } catch (Exception e) { throw new RuntimeException("Unable to determine program directory: ", e); } if (f.isDirectory()) { // Class is executed from class/package structure from file system return f; } else { // Class is executed from inside of a JAR -> f references the JAR // file return f.getParentFile(); } } /** * Saves <code>data</code> to the file specified by <code>filename</code>. * * @param filename * @param data * @throws IOException */ public static void saveBytes(String filename, byte[] data) throws IOException { FileOutputStream fo = null; try { fo = new FileOutputStream(filename); fo.write(data); } finally { closeStream(fo); } } /** * Saves <code>data</code> to the file specified by <code>filename</code>. * * @param filename * @param data * @return Data has been saved successfully? */ public static boolean saveBytesEx(String filename, byte[] data) { FileOutputStream fo = null; try { fo = new FileOutputStream(filename); fo.write(data); return true; } catch (IOException e) { return false; } finally { closeStream(fo); } } /** * Tries to delete a file or directory and throws an {@link IOException} if that fails. * * @param fileToDelete * @throws IOException * Thrown if <code>fileToDelete</code> can not be deleted. */ public static void deleteFile(File fileToDelete) throws IOException { if (!fileToDelete.delete()) throw new IOException("Deleting of \"" + fileToDelete + "\" failed."); } public static void renameFile(File oldFile, File newFile) throws IOException { if (!oldFile.renameTo(newFile)) throw new IOException("Failed to rename file: " + oldFile + " to " + newFile); } public static int getJavaMaxHeapMB() { try { return (int) (Runtime.getRuntime().maxMemory() / 1048576l); } catch (Exception e) { return -1; } } public static byte[] downloadHttpFile(String url) throws IOException { HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); int responseCode = conn.getResponseCode(); if (responseCode != HttpURLConnection.HTTP_OK) throw new IOException("Invalid HTTP response: " + responseCode + " for url " + conn.getURL()); InputStream in = conn.getInputStream(); try { return Utilities.getInputBytes(in); } finally { in.close(); } } public static void copyFile(File source, File target) throws IOException { FileChannel in = null; FileChannel out = null; try { in = (new FileInputStream(source)).getChannel(); out = (new FileOutputStream(target)).getChannel(); in.transferTo(0, source.length(), out); } finally { close(in); close(out); } } /** * * @param value * positive value * @return 0 if no bit is set else the highest bit that is one in <code>value</code> */ public static int getHighestBitSet(int value) { int bit = 0x40000000; for (int i = 31; i > 0; i--) { int test = bit & value; if (test != 0) return i; bit >>= 1; } return 0; } /** * * @param revsision * SVN revision string like <code>"1223"</code>, <code>"1224M"</code> or <code>"1616:1622M"</code> * @return parsed svn revision */ public static int parseSVNRevision(String revision) { revision = revision.trim(); int index = revision.lastIndexOf(':'); if (index >= 0) { revision = revision.substring(index + 1).trim(); } Matcher m = Pattern.compile("(\\d+)[^\\d]*").matcher(revision); if (!m.matches()) return -1; return Integer.parseInt(m.group(1)); } }