package com.kartoflane.superluminal2.utils; import java.util.Arrays; import java.util.Comparator; import java.util.Scanner; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; import com.kartoflane.superluminal2.components.Polygon; import com.kartoflane.superluminal2.components.enums.OS; import com.kartoflane.superluminal2.components.interfaces.Identifiable; import com.kartoflane.superluminal2.components.interfaces.Indexable; /** * This class contains various utility methods that didn't fit in the other util classes. * * @author kartoFlane * */ public class Utils { public static int distance(Point p1, Point p2) { return (int) Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); } public static int distance(int x1, int y1, int x2, int y2) { return (int) Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); } public static Point center(Point p1, Point p2) { return center(p1.x, p1.y, p2.x, p2.y); } public static Point center(int x1, int y1, int x2, int y2) { return new Point((x1 + x2) / 2, (y1 + y2) / 2); } public static Point add(Point p1, Point p2) { return new Point(p1.x + p2.x, p1.y + p2.y); } public static Rectangle copy(Rectangle r) { return new Rectangle(r.x, r.y, r.width, r.height); } public static Point copy(Point p) { return new Point(p.x, p.y); } public static RGB copy(RGB rgb) { return new RGB(rgb.red, rgb.green, rgb.blue); } public static int min(int a, int b, int c) { return Math.min(a, Math.min(b, c)); } public static int max(int a, int b, int c) { return Math.max(a, Math.max(b, c)); } public static int limit(int min, int number, int max) { return Math.max(min, Math.min(number, max)); } public static int sign(int x) { return x == 0 ? 0 : x / Math.abs(x); } /** * Values returned by {@link #angle(Point, Point)} sometimes need to be filtered through this * method in order to work correctly with regular Math functions... * * @param angle * angle, in degrees */ public static int convertAngle(int angle) { return angle * (-1) + 270; } /** * @see #convertAngle(int) */ public static double convertAngle(double angle) { return angle * (-1) + 270; } /** * @return random number in the specified range, both ends inclusive */ public static int random(int min, int max) { return (int) Math.round(random((double) min, max)); } /** * @see #random(int, int) */ public static double random(double min, double max) { return min + Math.random() * (max - min); } /** * Computes angle between the two points, in degrees.<br> * 0 means north, increases counter-clockwise. */ public static double angle(Point p1, Point p2) { double theta = Math.atan2(p1.x - p2.x, p1.y - p2.y); // theta += Math.PI / 2.0; double angle = Math.toDegrees(theta); if (angle < 0) { angle += 360; } return angle % 360; } /** * A = [0,1] = (x, y)<br> * B = [2,3] = (x + w, y)<br> * C = [4,5] = (x, y + h)<br> * D = [6,7] = (x + w, y + h)<br> * <br> * The result is a Z-like shape: * * <pre> * A--B * / * / * C--D * </pre> * * @return int array of length 8, containing alternating x and y coordinates, * describing the corners of the given rectangle. */ public static int[] toArray(Rectangle rect) { return new int[] { rect.x, rect.y, rect.x + rect.width, rect.y, rect.x, rect.y + rect.height, rect.x + rect.width, rect.y + rect.height }; } /** * The same as {@link #toArray(Rectangle)}, but 4th and 6th indices are swapped.<br> * The result is a rectangle-like shape: * * <pre> * A--B * | * D--C * </pre> * */ public static int[] toArrayPolygon(Rectangle rect) { return new int[] { rect.x, rect.y, rect.x + rect.width, rect.y, rect.x + rect.width, rect.y + rect.height, rect.x, rect.y + rect.height }; } /** * Rotates the rectangle around its center by the given angle. * * @param b * the rectangle to be rotated * @param rotation * angle in degrees * @return the rotated rectangle */ public static Rectangle rotate(Rectangle r, float rotation) { Rectangle b = copy(r); if (rotation % 180 == 0) { // no need to do anything } else if ((int) (rotation % 90) == 0) { b.x += b.width / 2 - b.height / 2; b.y += b.height / 2 - b.width / 2; int a = b.width; b.width = b.height; b.height = a; } else { Polygon p = new Polygon(toArrayPolygon(b)); p.rotate((float) Math.toRadians(rotation)); b = p.getBounds(); } return b; } /** * * @param point * the point to be rotated * @param the * point around which the point will be rotated * @param rad * angle in radians * @return the rotated point */ public static Point rotate(Point point, int cx, int cy, float rad) { Point p = copy(point); p.x = (int) (Math.cos(rad) * (point.x - cx) - Math.sin(rad) * (point.y - cy) + cx); p.y = (int) (Math.sin(rad) * (point.x - cx) + Math.cos(rad) * (point.y - cy) + cy); return p; } public static Point rotate(Point p, Point c, float rad) { return rotate(p, c.x, c.y, rad); } public static Point polar(Point origin, double rad, int distance) { Point result = new Point(0, 0); result.x = origin.x + (int) Math.round(Math.cos(rad) * distance); result.y = origin.y + (int) Math.round(Math.sin(rad) * distance); return result; } /** * Fixes the rectangle, so that width and height are always positive, but the * resulting rectangle covers the same area. * * @param r * the rectangle to be fixed (remains unchanged) * @return the fixed rectangle (new instance) */ public static Rectangle fix(Rectangle r) { return new Rectangle(Math.min(r.x, r.x + r.width), Math.min(r.y, r.y + r.height), Math.abs(r.width), Math.abs(r.height)); } /** * Tests whether a rectangle contains another. * * @param rect * the "larger" rectangle (that supposedly contains the "smaller" one) * @param other * the "smaller" rectangle (that supposedly is contained within the "larger" one) * @return true if the first rectangle contains the second, false otherwise. */ public static boolean contains(Rectangle rect, Rectangle other) { return rect.contains(other.x, other.y) && rect.contains(other.x + other.width, other.y + other.height); } public static int binarySearch(Identifiable[] array, String identifier, int min, int max) { if (min > max) return -1; int mid = (min + max) / 2; int result = identifier.compareTo(array[mid].getIdentifier()); if (result > 0) return binarySearch(array, identifier, mid + 1, max); else if (result < 0) return binarySearch(array, identifier, min, mid - 1); else return mid; } /** * Overlays the base RGB with the overlay RGB with the given opacity.<br> * It doesn't work terribly well with RGB (which is an additive color model), * but it's a good-enough approximation. * * @param base * the base color * @param overlay * the color that will be laid over base * @param alpha * the opacity of the overlay color. Only values from range 0.0 - 1.0. * @return the resulting color */ public static RGB tint(RGB base, RGB overlay, double alpha) { if (alpha < 0 || alpha > 1) throw new IllegalArgumentException("Alpha values must be within 0-1 range."); RGB tinted = new RGB(base.red, base.green, base.blue); tinted.red = (int) (((1 - alpha) * tinted.red + alpha * overlay.red)); tinted.green = (int) (((1 - alpha) * tinted.green + alpha * overlay.green)); tinted.blue = (int) (((1 - alpha) * tinted.blue + alpha * overlay.blue)); return tinted; } /** * For systems specified in the argument, wraps the string using the given settings. * For all others, just returns the same string (under assumption that they wrap the text themselves) * * @see #wrapOSNot(String, int, int, OS...) * @see #wrap(String, int, int) */ public static String wrapOS(String msg, int wrapWidth, int wrapTolerance, OS... wrapFor) { OS os = OS.identifyOS(); if (Arrays.asList(wrapFor).contains(os)) { return wrap(msg, wrapWidth, wrapTolerance); } else { return msg; } } /** * Exactly the same as {@link #wrapOS(String, int, int, OS...)}, except with inverted logic, * ie. for systems specified in the argument, the method <b>won't</b> wrap the string, but * will for all others. * * @see #wrapOS(String, int, int, OS...) * @see #wrap(String, int, int) */ public static String wrapOSNot(String msg, int wrapWidth, int wrapTolerance, OS... dontWrapFor) { OS os = OS.identifyOS(); if (Arrays.asList(dontWrapFor).contains(os)) { return msg; } else { return wrap(msg, wrapWidth, wrapTolerance); } } /** * Attempts to wrap the supplied string with the given width. * * @param msg * the message to be wrapped * @param wrapWidth * the maximum width of a single row of text. * Defaults to 50 if negative number or 0. * @param wrapTolerance * how many characters over the limit words can go. * Defaults to 5 if negative number. * @return wrapped string * * @see #wrapOS(String, int, int, OS...) * @see #wrapOSNot(String, int, int, OS...) */ public static String wrap(String msg, int wrapWidth, int wrapTolerance) { if (msg == null) throw new IllegalArgumentException("Argument must not be null."); if (wrapWidth <= 0) wrapWidth = 50; if (wrapTolerance < 0) wrapTolerance = 5; StringBuilder buf = new StringBuilder(); Scanner sc = new Scanner(msg); int currentWidth = 0; while (sc.hasNext()) { String token = sc.next(); int newWidth = currentWidth + token.length(); if (currentWidth >= wrapWidth) { // Even without the new token it's already outside -- move to new line buf.append("\n"); buf.append(token); currentWidth = token.length(); } else if (newWidth > wrapWidth + wrapTolerance) { // Falls outside of the allowed wrap width -- split String[] split = split(token, wrapWidth - currentWidth - 1); if (!split[0].isEmpty()) { buf.append(" "); buf.append(split[0]); } buf.append(split[0].isEmpty() || split[1].isEmpty() ? "\n" : "-\n"); buf.append(split[1]); currentWidth = split[1].length(); } else { // Fits within the wrap width -- append to current line if (currentWidth > 0) buf.append(" "); buf.append(token); currentWidth += token.length() + 1; } } return buf.toString(); } private static String[] split(String word, int splitIndex) { String[] result = new String[2]; if (splitIndex == 0) { result[0] = ""; result[1] = word; return result; } else if (splitIndex == word.length()) { result[0] = word; result[1] = ""; return result; } int i = findIndexOfClosestVowel(word, splitIndex); if (i < 0) { result[0] = word; result[1] = ""; } else { result[0] = word.substring(0, i); result[1] = word.substring(i); } return result; } private static int findIndexOfClosestVowel(String word, int splitIndex) { char[] vowels = { 'a', 'e', 'i', 'o', 'u', 'y' }; int result = -word.length(); for (char vowel : vowels) { int indexFrom = word.indexOf(vowel, splitIndex); int indexTo = word.substring(0, splitIndex - 1).indexOf(vowel); // Determine which one is closer int candidate = Math.abs(splitIndex - indexFrom) < Math.abs(splitIndex - indexTo) ? indexFrom : indexTo; result = Math.abs(splitIndex - result) < Math.abs(splitIndex - candidate) ? result : candidate; } return result; } public static boolean contains(Object[] array, Object object) { for (Object o : array) if (o.equals(object)) return true; return false; } public static int indexOf(Object[] array, Object object) { for (int i = 0; i < array.length; i++) { if (array[i].equals(object)) return i; } return -1; } /** * Reorders elements of the Indexable array, updating their IDs.<br> * ID of an indexable elements does not necessarily correspond to its index in the array. * * @param array * an array of Indexable objects * @param from * index of the element that is to be moved * @param to * destination index */ public static void reorder(Indexable[] array, int from, int to) { if (from < 0 || from >= array.length) throw new IndexOutOfBoundsException("" + from); if (to < 0 || to >= array.length) throw new IndexOutOfBoundsException("" + to); if (to == from) return; int dir = from > to ? 1 : -1; int oldId = array[to].getId(); /* * Update IDs of elements between the two indices * * The loop needs to go on as long as * (from > to && i < from) || (from < to && i > from) is true * Let's say A = (from > to); B = (i < from), then the above becomes: * (A && B) || (!A && !B) * Hence the simplified condition is A == B */ for (int i = to; from > to == i < from; i += dir) array[i].setId(array[Math.max(0, i + dir)].getId()); array[from].setId(oldId); } /** * The Alphanum Algorithm is an improved sorting algorithm for strings * containing numbers. Instead of sorting numbers in ASCII order like * a standard sort, this algorithm sorts numbers in numeric order. * * The Alphanum Algorithm is discussed at http://www.DaveKoelle.com * * This is an updated version with enhancements made by Daniel Migowski, * Andre Bogus, and David Koelle */ public static class AlphanumComparator implements Comparator<String> { private final boolean isDigit(char ch) { return ch >= 48 && ch <= 57; } /** Length of string is passed in for improved efficiency (only need to calculate it once) **/ private final String getChunk(String s, int slength, int marker) { StringBuilder chunk = new StringBuilder(); char c = s.charAt(marker); chunk.append(c); marker++; if (isDigit(c)) { while (marker < slength) { c = s.charAt(marker); if (!isDigit(c)) break; chunk.append(c); marker++; } } else { while (marker < slength) { c = s.charAt(marker); if (isDigit(c)) break; chunk.append(c); marker++; } } return chunk.toString(); } public int compare(String s1, String s2) { int thisMarker = 0; int thatMarker = 0; int s1Length = s1.length(); int s2Length = s2.length(); while (thisMarker < s1Length && thatMarker < s2Length) { String thisChunk = getChunk(s1, s1Length, thisMarker); thisMarker += thisChunk.length(); String thatChunk = getChunk(s2, s2Length, thatMarker); thatMarker += thatChunk.length(); // If both chunks contain numeric characters, sort them numerically int result = 0; if (isDigit(thisChunk.charAt(0)) && isDigit(thatChunk.charAt(0))) { // Simple chunk comparison by length. int thisChunkLength = thisChunk.length(); result = thisChunkLength - thatChunk.length(); // If equal, the first different number counts if (result == 0) { for (int i = 0; i < thisChunkLength; i++) { result = thisChunk.charAt(i) - thatChunk.charAt(i); if (result != 0) { return result; } } } } else { result = thisChunk.compareTo(thatChunk); } if (result != 0) return result; } return s1Length - s2Length; } } }