/******************************************************************************* * Copyright 2012 Geoscience Australia * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package au.gov.ga.earthsci.worldwind.common.util; import gov.nasa.worldwind.View; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.geom.Angle; import gov.nasa.worldwind.geom.LatLon; import gov.nasa.worldwind.geom.Position; import gov.nasa.worldwind.geom.Sector; import gov.nasa.worldwind.geom.Vec4; import gov.nasa.worldwind.geom.coords.MGRSCoord; import gov.nasa.worldwind.geom.coords.UTMCoordConverterAccessible; import gov.nasa.worldwind.globes.Globe; import gov.nasa.worldwind.util.Logging; import java.awt.Color; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.geom.Rectangle2D; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.Map; import java.util.Random; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipFile; /** * General utility methods. * * @author Michael de Hoog (michael.dehoog@ga.gov.au) * @author James Navin (james.navin@ga.gov.au) */ public class Util { /** The settings folder name to use for GA world wind settings */ public static final String SETTINGS_FOLDER_NAME = ".gaww"; public final static double METER_TO_FEET = 3.280839895; public final static double METER_TO_MILE = 0.000621371192; public final static String UTM_COORDINATE_REGEX = "(?:[a-zA-Z]*\\s*)(\\d+)(?:\\s*)([a-zA-Z])(?:\\s+)((?:\\d*\\.?\\d+)|(?:\\d+))(?:[E|e]?)(?:\\s+)((?:\\d*\\.?\\d+)|(?:\\d+))(?:[N|n]?)"; public final static String ELLIPSIS = "�"; /** * @return A string representation of the provided integer value, padded * with 0's to a total length of <code>charcount</code>. Note: If * length(value) > charcount, the input value will be returned as a * string. */ public static String paddedInt(int value, int charcount) { String str = String.valueOf(value); while (str.length() < charcount) { str = "0" + str; } return str; } /** * Create a URL pointing to a tile file on the local file system (or inside * a zip file). Returns null if no file for the tile was found. * * @param tile * Tile to search for a file for * @param context * Tile's layer's context URL * @param format * Tile's layer's default format * @param defaultExt * If the format is not given, search using this file extension * @return URL pointing to tile's file, or null if not found */ public static URL getLocalTileURL(String service, String dataset, int level, int row, int col, URL context, String format, String defaultExt) { if (dataset == null || dataset.length() <= 0) dataset = service; else if (service != null && service.length() > 0) dataset = service + "/" + dataset; if (dataset == null) dataset = ""; boolean isZip = true; int filenameLevel = 0; //first try a zip file at the root level: Ternary.zip File parent = Util.getPathWithinContext(dataset + ".zip", context); //next try a zip file at the level level: Ternary/1.zip if (parent == null) { parent = Util.getPathWithinContext(dataset + File.separator + level + ".zip", context); filenameLevel = 1; } //next try a zip file at the row level: Ternary/1/0002.zip if (parent == null) { parent = Util.getPathWithinContext( dataset + File.separator + level + File.separator + Util.paddedInt(row, 4) + ".zip", context); filenameLevel = 2; } //finally find a file in the standard tileset directory structure (no zip parent) if (parent == null) { parent = Util.getPathWithinContext(dataset, context); isZip = false; filenameLevel = 0; } if (parent == null) return null; //default to JPG String ext = defaultExt; if (format != null) { format = format.toLowerCase(); if (format.contains("jpg") || format.contains("jpeg")) ext = "jpg"; else if (format.contains("png")) ext = "png"; else if (format.contains("zip")) ext = "zip"; else if (format.contains("dds")) ext = "dds"; else if (format.contains("bmp")) ext = "bmp"; else if (format.contains("gif")) ext = "gif"; //for elevation models: else if (format.contains("bil")) ext = "bil"; else if (format.contains("zip")) ext = "zip"; } //build the filename relative to the parent level found above String filename = Util.paddedInt(row, 4) + "_" + Util.paddedInt(col, 4) + "." + ext; if (filenameLevel < 2) { filename = Util.paddedInt(row, 4) + File.separator + filename; if (filenameLevel < 1) { filename = level + File.separator + filename; } } try { if (parent.isFile() && isZip) { //zip file; return URL using 'jar' protocol String entry1 = filename; //if file is not found, attempt to find a file with the defaultExt in the zip as well String entry2 = ext.equals(defaultExt) ? null : filename.substring(0, filename.length() - ext.length()) + defaultExt; URL url = entry2 != null ? Util.zipEntryUrl(parent, entry1, entry2) : Util.zipEntryUrl(parent, entry1); return url; } else if (parent.isDirectory()) { //return standard 'file' protocol URL File file = new File(parent, filename); if (file.exists()) { return file.toURI().toURL(); } } } catch (MalformedURLException e) { String msg = "Converting tile file to URL failed"; Logging.logger().log(java.util.logging.Level.SEVERE, msg, e); } return null; } /** * Attempt to find a directory or file, relative to a given context URL */ private static File getPathWithinContext(String path, URL context) { //first attempt finding of the directory using a URL try { URL url = context == null ? new URL(path) : new URL(context, path); File file = URLUtil.urlToFile(url); if (file != null && file.exists()) return file; } catch (Exception e) { } //next try parsing the context to pull out a parent file File parent = null; if (context != null) { File file = URLUtil.urlToFile(context); if (file != null && file.isFile()) { parent = file.getParentFile(); if (parent != null && !parent.isDirectory()) parent = null; } } //if the parent isn't null, try using it as a parent file if (parent != null) { try { File file = new File(parent, path); if (file.exists()) return file; } catch (Exception e) { } } //otherwise ignore the parent and just attempt the path File file = new File(path); if (file.exists()) return file; return null; } /** * Return a URL which points to an entry within a zip file (or * <code>null</code> if none of the entries exist). * * @param zipFile * @param entries * Filenames within the zip file, returning the first entry found * (must be relative with no leading slash) * @return URL pointing to entry within zipFile * @throws MalformedURLException */ private static URL zipEntryUrl(File zipFile, String... entries) throws MalformedURLException { ZipFile zip = null; try { zip = new ZipFile(zipFile); for (String entry : entries) { entry = entry.replaceAll("\\\\", "/"); if (zip.getEntry(entry) != null) { URL zipFileUrl = zipFile.toURI().toURL(); return new URL("jar:" + zipFileUrl.toExternalForm() + "!/" + entry); } } } catch (MalformedURLException e) { throw e; } catch (IOException e) { //ignore } finally { if (zip != null) { try { zip.close(); } catch (IOException e) { //ignore } } } return null; } /** * @return A string containing random characters <code>[a-zA-z]</code> of * length <code>length</code> */ public static String randomString(int length) { String chars = new String("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); Random random = new Random(); StringBuilder sb = new StringBuilder(length); for (int i = 0; i < length; i++) { sb.append(chars.charAt(random.nextInt(chars.length()))); } return sb.toString(); } public static long getScaledLengthMillis(double scale, LatLon beginLatLon, LatLon endLatLon) { return getScaledLengthMillis(beginLatLon, endLatLon, (long) (3000 / scale), (long) (10000 / scale)); } public static long getScaledLengthMillis(double scale, double beginZoom, double endZoom) { return getScaledLengthMillis(beginZoom, endZoom, (long) (3000 / scale), (long) (10000 / scale)); } public static long getScaledLengthMillis(LatLon beginLatLon, LatLon endLatLon, long minLengthMillis, long maxLengthMillis) { Angle sphericalDistance = LatLon.greatCircleDistance(beginLatLon, endLatLon); double scaleFactor = angularRatio(sphericalDistance, Angle.POS180); return (long) mixDouble(scaleFactor, minLengthMillis, maxLengthMillis); } public static long getScaledLengthMillis(double beginZoom, double endZoom, long minLengthMillis, long maxLengthMillis) { double scaleFactor = Math.abs(endZoom - beginZoom) / Math.max(endZoom, beginZoom); scaleFactor = clamp(scaleFactor, 0.0, 1.0); return (long) mixDouble(scaleFactor, minLengthMillis, maxLengthMillis); } /** * Returns a 'mixing' of the values <code>value1</code> and * <code>value2</code> using <code>1-amount<code> of <code>value1</code> * linearly combined with <code>amount</code> of <code>value2</code>. * <code>amount</code> should be expressed as a percentage in the range * <code>[0,1]</code> */ public static double mixDouble(double amount, double value1, double value2) { if (amount < 0) return value1; else if (amount > 1) return value2; return value1 * (1.0 - amount) + value2 * amount; } /** * @return The provided value as a percentage of the interval * <code>[min, max]</code>, clamped to the interval * <code>[0,1]</code> */ public static double percentDouble(double value, double min, double max) { if (value < min) return 0; if (value > max) return 1; return (value - min) / (max - min); } public static double angularRatio(Angle x, Angle y) { if (x == null || y == null) { String message = Logging.getMessage("nullValue.AngleIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } double unclampedRatio = x.divide(y); return clamp(unclampedRatio, 0, 1); } public static Position computePositionFromString(String positionString) { return computePositionFromString(positionString, null); } public static Position computePositionFromString(String positionString, Globe globe) { int lastIndexOfSpace = positionString.trim().lastIndexOf(' '); if (lastIndexOfSpace < 0) return null; String latlonString = positionString.substring(0, lastIndexOfSpace); String elevationString = positionString.substring(lastIndexOfSpace + 1); double elevation; try { elevation = Double.parseDouble(elevationString); } catch (Exception e) { return null; } LatLon ll = computeLatLonFromString(latlonString, globe); if (ll == null) return null; return new Position(ll, elevation); } /** * @see #computeLatLonFromString(String, Globe) */ public static LatLon computeLatLonFromString(String coordString) { return computeLatLonFromString(coordString, null); } /** * Tries to extract a latitude and a longitude from the given text string. * <p/> * Supported formats are: * <ol> * <li>Comma- or space-separated signed decimal degrees, with optional * E/W/N/S suffixes (eg. <code>-97.345, 123.45</code> or * <code>97.345S 123.345W</code>)</li> * <li>Comma- or space-separated degree-minute-second blocks, with optional * E/W/N/S suffixes (eg. <code>-123� 34' 42", +45� 12' 30"</code> or * <code>123� 34' 42"S 45� 12' 30"W</code>)</li> * </ol> * <p/> * If the parsed Lat-Lons are outside of the valid range of * <code>Lat=[-90,90]</code> and <code>Lon=[-180,180]</code>, or parsing * fails, will return <code>null</code>. * * @param coordString * the input string * @param globe * the current <code>Globe</code> (Optional). * * @return the corresponding <code>LatLon</code> or <code>null</code>. */ public static LatLon computeLatLonFromString(String coordString, Globe globe) { if (isBlank(coordString)) { return null; } Angle lat = null; Angle lon = null; coordString = coordString.trim(); String regex; String separators = "(\\s*|,|,\\s*)"; Pattern pattern; Matcher matcher; // Try MGRS - allow spaces if (globe != null) { regex = "\\d{1,2}[A-Za-z]\\s*[A-Za-z]{2}\\s*\\d{1,5}\\s*\\d{1,5}"; if (coordString.matches(regex)) { try { MGRSCoord MGRS = MGRSCoord.fromString(coordString, globe); // NOTE: the MGRSCoord does not always report errors with invalid strings, // but will have lat and lon set to zero if (MGRS.getLatitude().degrees != 0 || MGRS.getLatitude().degrees != 0) { lat = MGRS.getLatitude(); lon = MGRS.getLongitude(); } else { return null; } } catch (IllegalArgumentException e) { return null; } } } // Try to extract a pair of signed decimal values separated by a space, ',' or ', ' // Allow E, W, S, N suffixes if (lat == null || lon == null) { regex = "([-|\\+]?\\d+?(\\.\\d+?)??\\s*[N|n|S|s]??)"; regex += separators; regex += "([-|\\+]?\\d+?(\\.\\d+?)??\\s*[E|e|W|w]??)"; pattern = Pattern.compile(regex); matcher = pattern.matcher(coordString); if (matcher.matches()) { String sLat = matcher.group(1).trim(); // Latitude int signLat = 1; char suffix = sLat.toUpperCase().charAt(sLat.length() - 1); if (!Character.isDigit(suffix)) { signLat = suffix == 'N' ? 1 : -1; sLat = sLat.substring(0, sLat.length() - 1); sLat = sLat.trim(); } String sLon = matcher.group(4).trim(); // Longitude int signLon = 1; suffix = sLon.toUpperCase().charAt(sLon.length() - 1); if (!Character.isDigit(suffix)) { signLon = suffix == 'E' ? 1 : -1; sLon = sLon.substring(0, sLon.length() - 1); sLon = sLon.trim(); } lat = Angle.fromDegrees(Double.parseDouble(sLat) * signLat); lon = Angle.fromDegrees(Double.parseDouble(sLon) * signLon); } } // Try to extract two degrees minute seconds blocks separated by a space, ',' or ', ' // Allow S, N, W, E suffixes and signs. // eg: -123� 34' 42" +45� 12' 30" // eg: 123� 34' 42"S 45� 12' 30"W if (lat == null || lon == null) { regex = "([-|\\+]?\\d{1,3}[d|D|\u00B0|\\s](\\s*\\d{1,2}[m|M|'|\u2019|\\s])?(\\s*\\d{1,2}[s|S|\"|\u201d])?\\s*[N|n|S|s]?)"; regex += separators; regex += "([-|\\+]?\\d{1,3}[d|D|\u00B0|\\s](\\s*\\d{1,2}[m|M|'|\u2019|\\s])?(\\s*\\d{1,2}[s|S|\"|\u201d])?\\s*[E|e|W|w]?)"; pattern = Pattern.compile(regex); matcher = pattern.matcher(coordString); if (matcher.matches()) { lat = parseDMSString(matcher.group(1)); lon = parseDMSString(matcher.group(5)); } } if (lat == null || lon == null) { return null; } if (lat.degrees >= -90 && lat.degrees <= 90 && lon.degrees >= -180 && lon.degrees <= 180) { return new LatLon(lat, lon); } return null; } /** * Parse a Degrees, Minute, Second coordinate string. * * @param dmsString * the string to parse. * @return the corresponding <code>Angle</code> or null. */ private static Angle parseDMSString(String dmsString) { // Replace degree, min and sec signs with space dmsString = dmsString.replaceAll("[D|d|\u00B0|'|\u2019|\"|\u201d]", " "); // Replace multiple spaces with single ones dmsString = dmsString.replaceAll("\\s+", " "); dmsString = dmsString.trim(); // Check for sign prefix and suffix int sign = 1; char suffix = dmsString.toUpperCase().charAt(dmsString.length() - 1); if (!Character.isDigit(suffix)) { sign = (suffix == 'N' || suffix == 'E') ? 1 : -1; dmsString = dmsString.substring(0, dmsString.length() - 1); dmsString = dmsString.trim(); } char prefix = dmsString.charAt(0); if (!Character.isDigit(prefix)) { sign *= (prefix == '-') ? -1 : 1; dmsString = dmsString.substring(1, dmsString.length()); } // Process degrees, minutes and seconds String[] DMS = dmsString.split(" "); double d = Integer.parseInt(DMS[0]); double m = DMS.length > 1 ? Integer.parseInt(DMS[1]) : 0; double s = DMS.length > 2 ? Integer.parseInt(DMS[2]) : 0; if (m >= 0 && m <= 60 && s >= 0 && s <= 60) return Angle.fromDegrees(d * sign + m / 60 * sign + s / 3600 * sign); return null; } /** * Parse and convert a UTM string to a LatLon point. * * @param coordString * @param globe * @param charRepresentsHemisphere * Does the character after the UTM zone represent the hemisphere * or the latitude band? * @return Point represented by UTM string */ public static LatLon computeLatLonFromUTMString(String coordString, Globe globe, boolean charRepresentsHemisphere) { coordString = coordString.trim(); Pattern pattern = Pattern.compile(UTM_COORDINATE_REGEX); Matcher matcher = pattern.matcher(coordString); if (matcher.matches()) { long zone = Long.parseLong(matcher.group(1)); char latitudeBand = matcher.group(2).toUpperCase().charAt(0); double easting = Double.parseDouble(matcher.group(3)); double northing = Double.parseDouble(matcher.group(4)); //if charRepresentsHemisphere, then latitudeBand will be 'N' or 'S' //otherwise, latitudeBand will be the actual latitudeBand //convert back to hemisphere strings: String hemisphere = charRepresentsHemisphere ? (latitudeBand <= 'N' ? AVKey.NORTH : AVKey.SOUTH) : (latitudeBand >= 'N' ? AVKey.NORTH : AVKey.SOUTH); try { final UTMCoordConverterAccessible converter = new UTMCoordConverterAccessible(globe); long err = converter.convertUTMToGeodetic(zone, hemisphere, easting, northing); if (err == UTMCoordConverterAccessible.UTM_NO_ERROR) { LatLon latlon = new LatLon(Angle.fromRadians(converter.getLatitude()), Angle.fromRadians(converter .getLongitude())); return latlon; } } catch (Exception e) { //ignore } } return null; } /** * @return The capitalised version of the provided string */ public static String capitalizeFirstLetter(String s) { if (isBlank(s)) { return s; } return s.substring(0, 1).toUpperCase() + s.substring(1); } /** * @return Whether the provided string is blank (<code>null</code>, empty * string, or contains only whitespace) */ public static boolean isBlank(String string) { return string == null || string.trim().isEmpty(); } /** * @return Whether the provided collection is <code>null</code> or empty */ public static boolean isEmpty(Collection<?> collection) { return collection == null || collection.isEmpty(); } /** * @return Whether the provided map is <code>null</code> or empty */ public static boolean isEmpty(Map<?, ?> map) { return map == null || map.isEmpty(); } /** * @return Whether the provided array is <code>null</code> or empty */ public static boolean isEmpty(Object[] array) { return array == null || array.length == 0; } /** * Clamp the provided value to the range specified by * <code>[min, max]</code> */ public static int clamp(int value, int min, int max) { if (min > max) { return clamp(value, max, min); } return Math.max(min, Math.min(max, value)); } /** * Clamp the provided value to the range specified by * <code>[min, max]</code> */ public static double clamp(double value, double min, double max) { if (min > max) { return clamp(value, max, min); } return Math.max(min, Math.min(max, value)); } /** * Clamp the provided value to the range specified by * <code>[min, max]</code> */ public static float clamp(float value, float min, float max) { if (min > max) { return clamp(value, max, min); } return Math.max(min, Math.min(max, value)); } /** * Clamp the provided {@link LatLon} pair to be within the provided * {@link Sector} extents. */ public static LatLon clampLatLon(LatLon latlon, Sector sector) { if (latlon == null || sector == null) { return latlon; } double lat = clamp(latlon.latitude.degrees, sector.getMinLatitude().degrees, sector.getMaxLatitude().degrees); double lon = clamp(latlon.longitude.degrees, sector.getMinLongitude().degrees, sector.getMaxLongitude().degrees); return LatLon.fromDegrees(lat, lon); } /** * Clamp the provided {@link Sector} to be within the provided * {@link Sector} extents. */ public static Sector clampSector(Sector source, Sector extents) { if (source == null || extents == null) { return source; } double minLat = clamp(source.getMinLatitude().degrees, extents.getMinLatitude().degrees, extents.getMaxLatitude().degrees); double maxLat = clamp(source.getMaxLatitude().degrees, extents.getMinLatitude().degrees, extents.getMaxLatitude().degrees); double minLon = clamp(source.getMinLongitude().degrees, extents.getMinLongitude().degrees, extents.getMaxLongitude().degrees); double maxLon = clamp(source.getMaxLongitude().degrees, extents.getMinLongitude().degrees, extents.getMaxLongitude().degrees); return Sector.fromDegrees(minLat, maxLat, minLon, maxLon); } /** * Calculate the position on the globe in the center of the view. If the * view is not looking at a point on the globe, the closest position is * found by calculating the horizon distance. * * @param view * View to find center position for * @param eyePoint * Eye point in model coordinates (if null, requested from view) * @return View's closest center position on the globe */ public static Position computeViewClosestCenterPosition(View view, Vec4 eyePoint) { Globe globe = view.getGlobe(); Vec4 centerPoint = view.getCenterPoint(); if (centerPoint != null) { return globe.computePositionFromPoint(centerPoint); } //center point is not on the globe, so compute the closest point on the horizon if (eyePoint == null) { eyePoint = view.getEyePoint(); } Vec4 forward = view.getForwardVector().normalize3(); Vec4 normal = globe.computeSurfaceNormalAtPoint(eyePoint); Vec4 left = normal.cross3(forward); forward = left.cross3(normal); double horizonDistance = view.getHorizonDistance(); centerPoint = eyePoint.add3(forward.multiply3(horizonDistance)); Position pos = globe.computePositionFromPoint(centerPoint); double elevation = globe.getElevation(pos.latitude, pos.longitude); return new Position(pos, elevation); } /** * Parse a Vec4 from a String representation of the form * <code>[(]x,[ ]y,[ ]z[,w][)]</code> */ public static Vec4 computeVec4FromString(String text) { if (isBlank(text)) { return null; } String separators = "[\\s,]+"; // Separate on commas or whitespace String[] split = text.replaceAll("\\(|\\)", "").trim().split(separators); // Clean up braces before splitting if (split.length == 3 || split.length == 4) { try { double x = Double.valueOf(split[0]); double y = Double.valueOf(split[1]); double z = Double.valueOf(split[2]); double w = 1d; if (split.length == 4) { w = Double.valueOf(split[3]); } return new Vec4(x, y, z, w); } catch (NumberFormatException e) { } } return null; } /** * @return The user's home directory */ public static File getUserGAWorldWindDirectory() { String home = System.getProperty("user.home"); File homeDir = new File(home); File dir = new File(homeDir, SETTINGS_FOLDER_NAME); if (!dir.exists()) { dir.mkdirs(); } return dir; } /** * Read the provided stream into a String */ public static String readStreamToString(InputStream stream) throws Exception { if (stream == null) { return null; } BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); StringBuffer result = new StringBuffer(); String readLine = null; while ((readLine = reader.readLine()) != null) { if (result.length() > 0) { result.append('\n'); } result.append(readLine); } reader.close(); return result.toString(); } /** * Convert an object to a Float. * * @param o * Object to convert * @return Float converted from object, or null if the object couldn't be * converted */ public static Float objectToFloat(Object o) { if (o == null) { return null; } if (o instanceof Float) { return (Float) o; } try { return Float.valueOf(o.toString()); } catch (NumberFormatException e) { return null; } } /** * Convert an object to a Double. * * @param o * Object to convert * @return Double converted from object, or null if the object couldn't be * converted */ public static Double objectToDouble(Object o) { if (o == null) { return null; } if (o instanceof Double) { return (Double) o; } try { return Double.valueOf(o.toString()); } catch (NumberFormatException e) { return null; } } public static Color interpolateColor(Color color0, Color color1, double mixer, boolean useHue) { if (color0 == null && color1 == null) return Color.black; if (color1 == null) return color0; if (color0 == null) return color1; if (mixer <= 0d) return color0; if (mixer >= 1d) return color1; if (useHue) { float[] hsb0 = Color.RGBtoHSB(color0.getRed(), color0.getGreen(), color0.getBlue(), null); float[] hsb1 = Color.RGBtoHSB(color1.getRed(), color1.getGreen(), color1.getBlue(), null); float h0 = hsb0[0]; float h1 = hsb1[0]; if (h1 < h0) h1 += 1f; if (h1 - h0 > 0.5f) h0 += 1f; float h = interpolateFloat(h0, h1, mixer); float s = interpolateFloat(hsb0[1], hsb1[1], mixer); float b = interpolateFloat(hsb0[2], hsb1[2], mixer); int alpha = interpolateInt(color0.getAlpha(), color1.getAlpha(), mixer); Color color = Color.getHSBColor(h, s, b); return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); } else { int r = interpolateInt(color0.getRed(), color1.getRed(), mixer); int g = interpolateInt(color0.getGreen(), color1.getGreen(), mixer); int b = interpolateInt(color0.getBlue(), color1.getBlue(), mixer); int a = interpolateInt(color0.getAlpha(), color1.getAlpha(), mixer); return new Color(r, g, b, a); } } public static int interpolateInt(int i0, int i1, double mixer) { return (int) Math.round(i0 * (1d - mixer) + i1 * mixer); } public static float interpolateFloat(float f0, float f1, double mixer) { return (float) (f0 * (1d - mixer) + f1 * mixer); } public static int indexInArray(Object[] array, Object object) { if (array == null) return -1; for (int i = 0; i < array.length; i++) { if (array[i] == object) return i; } return -1; } public static int nextLowestPowerOf2Plus1(int v) { //based on http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; v >>= 1; return ++v; } /** * Truncate a string until it fits in the given width. Tests string with * using the {@link FontMetrics} in the given {@link Graphics} object. * * @param s * String to truncate * @param width * Width to fit string in * @param g * Graphics object used to test string length * @param truncatedSuffix * Suffix to append onto the string if it is truncated * @return String that fits within the width. Returns null if null is * passed, or returns a blank string if no string will fit within * the given width. */ public static String stringByTruncatingToFitInWidth(String s, int width, Graphics g, String truncatedSuffix) { if (s == null) return null; if (truncatedSuffix == null) truncatedSuffix = ""; int length = s.length(); String truncated = s; while (length > 0) { Rectangle2D r = g.getFontMetrics().getStringBounds(truncated, g); if (r.getWidth() <= width) { return truncated; } length--; truncated = s.substring(0, length) + truncatedSuffix; } return ""; } /** * Calculate the previous power of 2. If x is a power of 2, x is returned, * otherwise the greatest power of two that is less than x is returned. * * @param x * @return Greatest power of two less than or equal to x */ public static int previousPowerOfTwo(int x) { x = x | (x >> 1); x = x | (x >> 2); x = x | (x >> 4); x = x | (x >> 8); x = x | (x >> 16); return x - (x >> 1); } }