added contents

This commit is contained in:
Mark Minas
2024-09-18 17:04:31 +02:00
parent d28b17eba5
commit 71a4ac8d12
176 changed files with 51198 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
plugins {
id 'buildlogic.java-library-conventions'
}
description = 'Common classes'

View File

@@ -0,0 +1,7 @@
handlers=java.util.logging.ConsoleHandler
.level=INFO
;pp.level=FINE
pp.util.triangulation.Polygon.level=FINE
java.util.logging.ConsoleHandler.level=FINER
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format=[%4$s %2$s] %5$s%6$s%n

View File

@@ -0,0 +1,257 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util;
import java.util.Objects;
import static pp.util.FloatMath.DEG_TO_RAD;
import static pp.util.FloatMath.FLT_EPSILON;
import static pp.util.FloatMath.RAD_TO_DEG;
import static pp.util.FloatMath.abs;
import static pp.util.FloatMath.atan2;
import static pp.util.FloatMath.cos;
import static pp.util.FloatMath.sin;
import static pp.util.FloatMath.sqrt;
/**
* A class for representing angles by unit vectors where angles define
* polar coordinates.
*/
public class Angle implements Comparable<Angle> {
/**
* 0 degrees, i.e., along the x-axis
*/
public static final Angle ZERO = new Angle(1f, 0f);
/**
* 180 degrees, i.e., along the negative x-axis
*/
public static final Angle PI = new Angle(-1f, 0f);
/**
* Returns the minimum of the specified angles with respect to
* {@linkplain #compareTo(Angle)}.
*
* @param u first angle to compare
* @param v second angle to compare
*/
public static Angle min(Angle u, Angle v) {
return u.compareTo(v) <= 0 ? u : v;
}
/**
* The x-coordinate of the unit vector.
*/
public final float x;
/**
* The y-coordinate of the unit vector.
*/
public final float y;
/**
* Creates a new angle represented by the vector with the specified
* coordinates. Note that the vector must have length 1.
*/
private Angle(float x, float y) {
this.x = x;
this.y = y;
}
/**
* Returns the polar coordinates angle of the specified vector.
*
* @param x the vectors x value
* @param y the vectors y value
*/
public static Angle fromVector(float x, float y) {
final float len = sqrt(x * x + y * y);
if (len < FLT_EPSILON)
throw new IllegalArgumentException("null vector");
return new Angle(x / len, y / len);
}
/**
* Returns the polar coordinates angle when looking from the first to the second
* specified position.
*
* @param from first position
* @param to second position
*/
public static Angle direction(Position from, Position to) {
return fromVector(to.getX() - from.getX(),
to.getY() - from.getY());
}
/**
* Returns an Angle object for the specified angle in radians.
*
* @param radians the specified radian value
*/
public static Angle fromRadians(float radians) {
return new Angle(cos(radians), sin(radians));
}
/**
* Returns an Angle object for the specified angle in degrees.
*
* @param degrees the specified degrees value
*/
public static Angle fromDegrees(float degrees) {
return fromRadians(degrees * DEG_TO_RAD);
}
/**
* Returns the value of this angle in radians in the range (-pi,pi].
*/
public float radians() {
return atan2(y, x);
}
/**
* Returns the value of this angle in degrees in the range (-180,180].
*/
public float degrees() {
return radians() * RAD_TO_DEG;
}
/**
* Returns the x-coordinate of the unit vector, that is the cosine of the angle.
*/
public float getX() {
return x;
}
/**
* Returns the y-coordinate of the unit vector, that is the sinus of the angle.
*/
public float getY() {
return y;
}
/**
* Returns the angle obtained by adding the specified angle to this angle.
*
* @param o the other angle
*/
public Angle plus(Angle o) {
return new Angle(x * o.x - y * o.y,
x * o.y + y * o.x);
}
/**
* Returns the angle obtained by subtracting the specified angle from this angle.
*
* @param o the other angle
*/
public Angle minus(Angle o) {
return new Angle(y * o.y + x * o.x,
y * o.x - x * o.y);
}
/**
* Returns the bisector angle between this angle as left angle and the
* specified right angle, that is, the angle halfway from the right to this
* angle when turning from right to left.
*
* @param right right angle
* @return the bisector angle between this as left and the specified right angle
*/
public Angle bisector(Angle right) {
// compute vector of this.minus(right)
final float dx = y * right.y + x * right.x;
final float dy = y * right.x - x * right.y;
if (abs(dy) < FLT_EPSILON) {
// the difference is either 0° or 180°
if (dx > 0f)
return this;
else
return new Angle(-right.y, right.x);
}
final float mid = 0.5f * atan2(dy, dx);
final float sum = right.radians() + mid;
if (mid > 0f)
return new Angle(cos(sum), sin(sum));
else
return new Angle(-cos(sum), -sin(sum));
}
/**
* Returns true if turning left from the specified angle towards this angle is
* shorter than turning right towards this angle.
*
* @param o another angle
*/
public boolean leftOf(Angle o) {
return y * o.x - x * o.y > 0f;
}
/**
* Returns true if turning right from the specified angle towards this angle is
* shorter than turning left towards this angle.
*
* @param o another angle
*/
public boolean rightOf(Angle o) {
return y * o.x - x * o.y < 0f;
}
/**
* Compares this angle with the specified one and returns -1, 0, or 1 if
* the value of this angle is less than, equal to, or greater than the value
* of the specified angle, respectively, where angle values are in the
* range [0,2*pi) and 0 means along the x-axis.
*
* @param o the other angle
*/
@Override
public int compareTo(Angle o) {
if (y == 0f) {
if (o.y < 0f)
return -1;
if (o.y > 0f)
return x > 0f ? -1 : 1;
if (x > 0f)
return o.x > 0f ? 0 : 1;
return o.x > 0f ? -1 : 0;
}
if (y > 0f) {
if (o.y < 0f)
return -1;
if (o.y == 0f)
return o.x > 0f ? 1 : -1;
}
else if (o.y >= 0f)
return 1;
final float det = x * o.y - y * o.x;
if (det == 0f)
return 0;
return det > 0f ? -1 : 1;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof Angle angle)
return compareTo(angle) == 0;
return false;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
/**
* Returns a string representation of this angle in degrees in the range [0,2*pi).
*/
@Override
public String toString() {
if (degrees() < 0f)
return (degrees() + 360f) + "°";
return degrees() + "°";
}
}

View File

@@ -0,0 +1,425 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util;
/**
* Provides mathematical functions using float precision.
*/
public class FloatMath {
private FloatMath() { /* don't instantiate */ }
public static final double DBL_EPSILON = 2.220446049250313E-16d;
/**
* A "close to zero" float epsilon value for use
*/
public static final float FLT_EPSILON = 1.1920928955078125E-7f;
/**
* A "close to zero" float epsilon value for use
*/
public static final float ZERO_TOLERANCE = 0.0001f;
/**
* The value 1/3, as a float.
*/
public static final float ONE_THIRD = 1f / 3f;
/**
* The value PI as a float. (180 degrees)
*/
public static final float PI = (float) Math.PI;
/**
* The value 2PI as a float. (360 degrees)
*/
public static final float TWO_PI = 2.0f * PI;
/**
* The value PI/2 as a float. (90 degrees)
*/
public static final float HALF_PI = 0.5f * PI;
/**
* The value PI/4 as a float. (45 degrees)
*/
public static final float QUARTER_PI = 0.25f * PI;
/**
* The value 1/PI as a float.
*/
public static final float INV_PI = 1.0f / PI;
/**
* The value 1/(2PI) as a float.
*/
public static final float INV_TWO_PI = 1.0f / TWO_PI;
/**
* A value to multiply a degree value by, to convert it to radians.
*/
public static final float DEG_TO_RAD = PI / 180.0f;
/**
* A value to multiply a radian value by, to convert it to degrees.
*/
public static final float RAD_TO_DEG = 180.0f / PI;
/**
* Linear interpolation from startValue to endValue by the given percent.
* Basically: ((1 - percent) * startValue) + (percent * endValue)
*
* @param scale scale value to use. if 1, use endValue, if 0, use startValue.
* @param startValue Beginning value. 0% of f
* @param endValue ending value. 100% of f
* @return The interpolated value between startValue and endValue.
*/
public static float interpolateLinear(float scale, float startValue, float endValue) {
if (startValue == endValue) {
return startValue;
}
if (scale <= 0f) {
return startValue;
}
if (scale >= 1f) {
return endValue;
}
return ((1f - scale) * startValue) + (scale * endValue);
}
/**
* Linear extrapolation from startValue to endValue by the given scale.
* if scale is between 0 and 1 this method returns the same result as interpolateLinear
* if the scale is over 1 the value is linearly extrapolated.
* Note that the end value is the value for a scale of 1.
*
* @param scale the scale for extrapolation
* @param startValue the starting value (scale = 0)
* @param endValue the end value (scale = 1)
* @return an extrapolation for the given parameters
*/
public static float extrapolateLinear(float scale, float startValue, float endValue) {
return ((1f - scale) * startValue) + (scale * endValue);
}
/**
* Returns the arc cosine of a value.<br>
* Special cases:
* <ul><li>If fValue is smaller than -1, then the result is PI.
* <li>If the argument is greater than 1, then the result is 0.</ul>
*
* @param fValue The value to arc cosine.
* @return The angle, in radians.
* @see Math#acos(double)
*/
public static float acos(float fValue) {
if (-1.0f < fValue) {
if (fValue < 1.0f) {
return (float) Math.acos(fValue);
}
return 0.0f;
}
return PI;
}
/**
* Returns the arc sine of a value.<br>
* Special cases:
* <ul><li>If fValue is smaller than -1, then the result is -HALF_PI.
* <li>If the argument is greater than 1, then the result is HALF_PI.</ul>
*
* @param fValue The value to arc sine.
* @return the angle in radians.
* @see Math#asin(double)
*/
public static float asin(float fValue) {
if (-1.0f < fValue) {
if (fValue < 1.0f) {
return (float) Math.asin(fValue);
}
return HALF_PI;
}
return -HALF_PI;
}
/**
* Returns the arc tangent of an angle given in radians.<br>
*
* @param fValue The angle, in radians.
* @return fValue's atan
* @see Math#atan(double)
*/
public static float atan(float fValue) {
return (float) Math.atan(fValue);
}
/**
* A direct call to Math.atan2.
*
* @param fY ordinate
* @param fX abscissa
* @return Math.atan2(fY, fX)
* @see Math#atan2(double, double)
*/
public static float atan2(float fY, float fX) {
return (float) Math.atan2(fY, fX);
}
public static float sinh(float x) {
return (float) Math.sinh(x);
}
public static float cosh(float x) {
return (float) Math.cosh(x);
}
public static float tanh(float x) {
return (float) Math.tanh(x);
}
public static float coth(float x) {
return (float) (1d / Math.tanh(x));
}
public static float arsinh(float x) {
return log(x + sqrt(x * x + 1f));
}
public static float arcosh(float x) {
return log(x + sqrt(x * x - 1f));
}
public static float artanh(float x) {
return 0.5f * log((1f + x) / (1f - x));
}
public static float arcoth(float x) {
return 0.5f * log((x + 1f) / (x - 1f));
}
/**
* Rounds a fValue up. A call to Math.ceil
*
* @param fValue The value.
* @return The fValue rounded up
* @see Math#ceil(double)
*/
public static float ceil(float fValue) {
return (float) Math.ceil(fValue);
}
/**
* Returns cosine of an angle. Direct call to Math
*
* @param v The angle to cosine.
* @return the cosine of the angle.
* @see Math#cos(double)
*/
public static float cos(float v) {
return (float) Math.cos(v);
}
/**
* Returns the sine of an angle. Direct call to Math
*
* @param v The angle to sine.
* @return the sine of the angle.
* @see Math#sin(double)
*/
public static float sin(float v) {
return (float) Math.sin(v);
}
/**
* Returns E^fValue
*
* @param fValue Value to raise to a power.
* @return The value E^fValue
* @see Math#exp(double)
*/
public static float exp(float fValue) {
return (float) Math.exp(fValue);
}
public static float expm1(float fValue) {
return (float) Math.expm1(fValue);
}
/**
* Returns Absolute value of a float.
*
* @param fValue The value to abs.
* @return The abs of the value.
* @see Math#abs(float)
*/
public static float abs(float fValue) {
if (fValue < 0) {
return -fValue;
}
return fValue;
}
/**
* Returns a number rounded down.
*
* @param fValue The value to round
* @return The given number rounded down
* @see Math#floor(double)
*/
public static float floor(float fValue) {
return (float) Math.floor(fValue);
}
/**
* Returns 1/sqrt(fValue)
*
* @param fValue The value to process.
* @return 1/sqrt(fValue)
* @see Math#sqrt(double)
*/
public static float invSqrt(float fValue) {
return (float) (1.0f / Math.sqrt(fValue));
}
/**
* Quickly estimate 1/sqrt(fValue).
*
* @param x the input value (&ge;0)
* @return an approximate value for 1/sqrt(x)
*/
public static float fastInvSqrt(float x) {
float halfX = 0.5f * x;
int i = Float.floatToIntBits(x); // get bits for floating value
i = 0x5f375a86 - (i >> 1); // gives initial guess y0
x = Float.intBitsToFloat(i); // convert bits back to float
x = x * (1.5f - halfX * x * x); // Newton step, repeating increases accuracy
return x;
}
/**
* Returns the log base E of a value.
*
* @param fValue The value to log.
* @return The log of fValue base E
* @see Math#log(double)
*/
public static float log(float fValue) {
return (float) Math.log(fValue);
}
/**
* Returns a number raised to an exponent power. fBase^fExponent
*
* @param fBase The base value (IE 2)
* @param fExponent The exponent value (IE 3)
* @return base raised to exponent (IE 8)
* @see Math#pow(double, double)
*/
public static float pow(float fBase, float fExponent) {
return (float) Math.pow(fBase, fExponent);
}
/**
* Returns the value squared. fValue ^ 2
*
* @param fValue The value to square.
* @return The square of the given value.
*/
public static float sqr(float fValue) {
return fValue * fValue;
}
/**
* Returns the square root of a given value.
*
* @param fValue The value to sqrt.
* @return The square root of the given value.
* @see Math#sqrt(double)
*/
public static float sqrt(float fValue) {
return (float) Math.sqrt(fValue);
}
/**
* Returns the tangent of the specified angle.
*
* @param fValue The value to tangent, in radians.
* @return The tangent of fValue.
* @see Math#tan(double)
*/
public static float tan(float fValue) {
return (float) Math.tan(fValue);
}
/**
* Returns 1 if the number is positive, -1 if the number is negative, and 0 otherwise
*
* @param iValue The integer to examine.
* @return The integer's sign.
*/
public static int sign(int iValue) {
return Integer.compare(iValue, 0);
}
/**
* Returns 1 if the number is positive, -1 if the number is negative, and 0 otherwise
*
* @param fValue The float to examine.
* @return The float's sign.
*/
public static float sign(float fValue) {
return Math.signum(fValue);
}
/**
* Take a float input and clamp it between min and max.
*
* @param input the value to be clamped
* @param min the minimum output value
* @param max the maximum output value
* @return clamped input
*/
public static float clamp(float input, float min, float max) {
return Math.max(min, Math.min(input, max));
}
/**
* Clamps the given float to be between 0 and 1.
*
* @param input the value to be clamped
* @return input clamped between 0 and 1.
*/
public static float saturate(float input) {
return clamp(input, 0f, 1f);
}
/**
* Determine if two floats are approximately equal.
* This takes into account the magnitude of the floats, since
* large numbers will have larger differences be close to each other.
* <p>
* Should return true for a=100000, b=100001, but false for a=10000, b=10001.
*
* @param a The first float to compare
* @param b The second float to compare
* @return True if a and b are approximately equal, false otherwise.
*/
public static boolean approximateEquals(float a, float b) {
if (a == b) {
return true;
}
else {
return (abs(a - b) / Math.max(abs(a), abs(b))) <= 0.00001f;
}
}
/**
* Normalizes the specified angle to lie in the interval (-pi,pi] and returns the normalized value in radians.
*
* @param angle the specified angle in radians
*/
public static float normalizeAngle(float angle) {
final float res = angle % TWO_PI;
if (res <= -FloatMath.PI) return res + TWO_PI;
else if (res > FloatMath.PI) return res - TWO_PI;
return res;
}
}

View File

@@ -0,0 +1,47 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util;
/**
* A trivial implementation of points in the plane with float coordinates.
*
* @param x x-coordinate
* @param y y-coordinate
*/
public record FloatPoint(float x, float y) implements Position {
public static final FloatPoint ZERO = new FloatPoint(0f, 0f);
/**
* Create a new FloatPoint object for the given position.
*
* @param p a position
*/
public FloatPoint(Position p) {
this(p.getX(), p.getY());
}
/**
* Returns the x-coordinate.
*/
@Override
public float getX() {
return x;
}
/**
* Returns the y-coordinate.
*/
@Override
public float getY() {
return y;
}
public static Position p(float x, float y) {
return new FloatPoint(x, y);
}
}

View File

@@ -0,0 +1,77 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util;
import static pp.util.FloatMath.ZERO_TOLERANCE;
import static pp.util.FloatMath.abs;
/**
* Represents an interval. The start value must not be greater than the end value.
*
* @param from the start value of the interval
* @param to the end value of the interval
*/
public record Interval(float from, float to) {
/**
* Creates a new interval between two specified values.
*
* @param from the specified start value
* @param to the specified end value
*/
public Interval {
if (from > to)
throw new IllegalArgumentException(from + " > " + to);
}
/**
* Checks whether this interval has length 0, i.e., from and to are equal.
*/
public boolean isEmpty() {
return to == from;
}
/**
* Checks whether the specified value is contained in this interval. Note that an empty interval may
* contain the value if the value is the start and the end value of the interval.
*
* @param value the specified value to check
*/
public boolean contains(float value) {
return from - value <= ZERO_TOLERANCE && value - to <= ZERO_TOLERANCE;
}
/**
* Checks whether the specified interval is contained as a sub-interval.
*
* @param other the potential sub-interval
*/
public boolean contains(Interval other) {
return from - other.from < ZERO_TOLERANCE && other.to - to < ZERO_TOLERANCE;
}
/**
* Returns a string representation of this interval.
*/
@Override
public String toString() {
return "[" + from + "; " + to + "]";
}
/**
* Checks whether the specified interval is almost equal to this
* interval up to the specified epsilon value.
*
* @param other the other interval to check
* @param eps the allowed epsilon value
*/
public boolean matches(Interval other, float eps) {
return abs(from - other.from) < eps &&
abs(to - other.to) < eps;
}
}

View File

@@ -0,0 +1,84 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util;
import static pp.util.FloatMath.sqrt;
/**
* Interface for all objects that provide a position in the plane.
*/
public interface Position extends Comparable<Position> {
/**
* Returns the x-coordinate of the position
*
* @return x-coordinate as float
*/
float getX();
/**
* Returns the y-coordinate of the position
*
* @return y-coordinate as float
*/
float getY();
/**
* Returns the distance of this position from the specified position.
*
* @param x x-coordinate of the position
* @param y y-coordinate of the position
* @return distance
*/
default float distanceTo(float x, float y) {
return sqrt(distanceSquaredTo(x, y));
}
/**
* Returns the distance of this position from the specified position.
* This is just a convenience method for {@linkplain #distanceTo(float, float)}.
*
* @param other the other position
* @return distance
*/
default float distanceTo(Position other) {
return distanceTo(other.getX(), other.getY());
}
/**
* Returns the squared distance of this position from the specified position.
*
* @param x x-coordinate of the position
* @param y y-coordinate of the position
* @return squared distance
*/
default float distanceSquaredTo(float x, float y) {
final float dx = getX() - x;
final float dy = getY() - y;
return dx * dx + dy * dy;
}
/**
* Returns the squared distance of this position from the specified position.
*
* @param p the other position
* @return squared distance
*/
default float distanceSquaredTo(Position p) {
return distanceSquaredTo(p.getX(), p.getY());
}
/**
* Compares positions in the plane from top to bottom
* (y coordinates grow downwards) and then from left ro right.
*/
@Override
default int compareTo(Position other) {
final int c = Float.compare(getY(), other.getY());
return c != 0 ? c : Float.compare(getX(), other.getX());
}
}

View File

@@ -0,0 +1,30 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util;
import java.util.prefs.Preferences;
/**
* A class with convenience methods for preferences.
*/
public class PreferencesUtils {
private PreferencesUtils() {
// don't instantiate
}
/**
* Returns a preferences node for the specified class object. The path of the
* preference node corresponds to the fully qualified name of the class.
*
* @param clazz a class object
* @return a preference node for the specified class
*/
public static Preferences getPreferences(Class<?> clazz) {
return Preferences.userNodeForPackage(clazz).node(clazz.getSimpleName());
}
}

View File

@@ -0,0 +1,90 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Random;
/**
* An iterator that creates a random permutation of positions (x, y) where x and y are integer values
* in the range {0, ..., width-1} x {0, ..., height-1}. All permutations are uniformly distributed
* using the FisherYates shuffle (also known as Knuth shuffle).
*
* @param <T> the type of elements returned by this iterator
*/
public class RandomPositionIterator<T> implements Iterator<T> {
private final Random random = new Random();
private final int height;
private final Map<Integer, T> movedMap = new HashMap<>();
private int remaining;
private final Creator<T> creator;
/**
* Functional interface to create instances of type T.
*
* @param <T> the type of elements created by this creator
*/
public interface Creator<T> {
T create(int x, int y);
}
/**
* Creates a RandomPositionIterator for Position instances with float coordinates.
*
* @param width the width of the rectangle
* @param height the height of the rectangle
* @return a RandomPositionIterator for Position instances
*/
public static RandomPositionIterator<Position> floatPoints(int width, int height) {
return new RandomPositionIterator<>(FloatPoint::new, width, height);
}
/**
* Creates a new permutation iterator generating a random permutation of positions (x, y)
* where x and y are integer values in the range {0, ..., width-1} x {0, ..., height-1}.
*
* @param creator the creator to create instances of type T
* @param width the width of the rectangle
* @param height the height of the rectangle
*/
public RandomPositionIterator(Creator<T> creator, int width, int height) {
this.height = height;
this.remaining = width * height;
this.creator = creator;
}
@Override
public boolean hasNext() {
return remaining > 0;
}
@Override
public T next() {
if (hasNext()) {
final int idx = random.nextInt(remaining--); // note that remaining is decremented
final T result = getWhere(idx);
if (idx < remaining)
movedMap.put(idx, getWhere(remaining));
movedMap.remove(remaining);
return result;
}
throw new NoSuchElementException();
}
private T getWhere(int idx) {
final T movedWhere = movedMap.get(idx);
if (movedWhere != null)
return movedWhere;
final int x = idx / height;
final int y = idx % height;
return creator.create(x, y);
}
}

View File

@@ -0,0 +1,16 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util;
/**
* A directed straight line segment between two positions in the plane.
*
* @param from the start position of the segment
* @param to the end position of the segment
*/
public record Segment(Position from, Position to) implements SegmentLike {}

View File

@@ -0,0 +1,396 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util;
import static java.lang.Float.max;
import static java.lang.Float.min;
import static pp.util.FloatMath.FLT_EPSILON;
import static pp.util.FloatMath.ZERO_TOLERANCE;
import static pp.util.FloatMath.abs;
import static pp.util.FloatMath.atan2;
import static pp.util.FloatMath.cos;
import static pp.util.FloatMath.sin;
import static pp.util.FloatMath.sqr;
import static pp.util.FloatMath.sqrt;
/**
* Interface of geometrical objects like segments, i.e., having a start and an end position.
*/
public interface SegmentLike {
/**
* Returns the start position of the segment.
*/
Position from();
/**
* Returns the end position of the segment.
*/
Position to();
/**
* Returns the length, i.e., the distance between the start and the end point of this segment.
*
* @return length of the line segment
*/
default float length() {
return sqrt(lengthSquared());
}
/**
* Returns the length squared where the length is the distance between the start and the
* end point of this segment.
*
* @return length squared of the line segment
*/
default float lengthSquared() {
return lengthSquared(from(), to());
}
/**
* Returns the squared length of the vector between the specified points.
*/
static float lengthSquared(Position from, Position to) {
final float dx = to.getX() - from.getX();
final float dy = to.getY() - from.getY();
return dx * dx + dy * dy;
}
/**
* Returns the length of the vector between the specified points.
*/
static float length(Position from, Position to) {
return sqrt(lengthSquared(from, to));
}
/**
* Returns the angle of this line segment with the x-axis.
*
* @return angle with the x-axis
*/
default float angle() {
final float dx = to().getX() - from().getX();
final float dy = to().getY() - from().getY();
return atan2(dy, dx);
}
/**
* Returns the distance of the specified point from this segment.
* The distance is the length of the shortest connection between the specified
* point and any point of the line segment.
*
* @param x x-coordinate of the point
* @param y y-coordinate of the point
* @return the distance
*/
default float distanceTo(float x, float y) {
return distance(from().getX(), from().getY(), to().getX(), to().getY(), x, y);
}
/**
* Returns the distance of the point from the segment between the specified points.
* The distance is the length of the shortest connection between the specified
* point and any point of the line segment.
*
* @param from the segment start point
* @param to the segment end point
* @param p the point
* @return the distance
*/
static float distance(Position from, Position to, Position p) {
return distance(from.getX(), from.getY(), to.getX(), to.getY(), p.getX(), p.getY());
}
/**
* Returns the distance of the point (x,y) from the segment from (x1,y1) to (x2,y2)
* The distance is the length of the shortest connection between the specified
* point and any point of the line segment.
*
* @param x1 x-coordinate of the segment start point
* @param y1 y-coordinate of the segment start point
* @param x2 x-coordinate of the segment end point
* @param y2 y-coordinate of the segment end point
* @param x x-coordinate of the point
* @param y y-coordinate of the point
* @return the distance
*/
static float distance(float x1, float y1, float x2, float y2, float x, float y) {
final float dx = x2 - x1;
final float dy = y2 - y1;
final float dx1 = x - x1;
final float dy1 = y - y1;
if (dx * dx1 + dy * dy1 <= 0f)
return sqrt(dx1 * dx1 + dy1 * dy1);
final float dx2 = x - x2;
final float dy2 = y - y2;
if (dx * dx2 + dy * dy2 >= 0f)
return sqrt(dx2 * dx2 + dy2 * dy2);
final float len = sqrt(dx * dx + dy * dy);
return FloatMath.abs(dx1 * dy - dy1 * dx) / len;
}
/**
* Returns the distance of the specified position from this segment.
* This is just a convenience method for {@linkplain #distanceTo(float, float)}.
*
* @param pos a position
* @return the distance
*/
default float distanceTo(Position pos) {
return distanceTo(pos.getX(), pos.getY());
}
/**
* Returns the point of this segment with the specified quotient, i.e., q*from()+(1-q)*to().
*
* @param q the quotient
*/
default Position pointAt(float q) {
if (q == 0f) return from();
if (q == 1f) return to();
return new FloatPoint((1f - q) * from().getX() + q * to().getX(),
(1f - q) * from().getY() + q * to().getY());
}
/**
* Shoots a ray from the specified position in the direction of the specified angle and returns the distance
* of the specified position from the intersection point of the ray with the straight line determined by the
* end points of this segment. Returns {@linkplain Float#NaN} if there is no intersection.
*
* @param pos the specified position
* @param angle the specified angle
*/
default float dist(Position pos, float angle) {
return quotientDist(pos, quotient(pos, angle));
}
/**
* Shoots a ray from the specified position in the direction of the specified angle and returns the distance
* of the specified position from the intersection point of the ray with the straight line determined by the
* end points of this segment. Returns {@linkplain Float#NaN} if there is no intersection.
*
* @param pos the specified position
* @param angle the specified angle
*/
default float dist(Position pos, Angle angle) {
return quotientDist(pos, quotient(pos, angle.x, angle.y));
}
/**
* Shoots a ray from the specified position in the direction of the point of this segment with the specified
* quotient, i.e., the point at q*from()+(1-q)*to(), and returns the distance
* of the specified position from the intersection point of the ray with the straight line determined by the
* end points of this segment. Returns {@linkplain Float#NaN} if there is no intersection.
*
* @param pos the specified position
* @param q the specified quotient
*/
default float quotientDist(Position pos, float q) {
final float dx = (1f - q) * from().getX() + q * to().getX() - pos.getX();
final float dy = (1f - q) * from().getY() + q * to().getY() - pos.getY();
return sqrt(dx * dx + dy * dy);
}
/**
* Shoots a ray from the specified position in the direction of the specified angle and returns the
* quotient q such that the intersection point of the ray with the straight line determined by the
* end points of this segment is at q*from()+(1-q)*to().
*
* @param pos the specified position
* @param angle the specified angle
*/
default float quotient(Position pos, float angle) {
final float ux = cos(angle);
final float uy = sin(angle);
return quotient(pos, ux, uy);
}
/**
* Shoots a ray from the specified position in the direction of the specified vector and returns the
* quotient q such that the intersection point of the ray with the straight line determined by the
* end points of this segment is at q*from()+(1-q)*to().
*
* @param pos the specified position
* @param ux the vectors x value
* @param uy the vectors y value
*/
private float quotient(Position pos, float ux, float uy) {
final float nom = nominator(pos, ux, uy);
final float det = determinant(ux, uy);
// the following is for dealing with floating point imprecision
if (abs(det) > FLT_EPSILON)
return nom / det;
if (abs(nom) > FLT_EPSILON)
return Float.NaN;
final float q = project(pos);
if (q > -FLT_EPSILON && q - 1f < FLT_EPSILON)
// pos lies (almost) within the segment
return q;
final float distFrom = isCandidate(pos, ux, uy, from());
final float distTo = isCandidate(pos, ux, uy, to());
if (distFrom >= 0f) {
if (distTo >= 0f)
return distFrom < distTo ? 0f : 1f;
else
return 0f;
}
if (distTo >= 0f)
return 1f;
return Float.NaN;
}
/**
* Returns the determinant of a specified vector.
*
* @param ux the vectors x value
* @param uy the vectors y value
*/
private float determinant(float ux, float uy) {
return diffX() * uy - diffY() * ux;
}
/**
* Returns the nominator of the specified vector starting at a specified position.
*
* @param pos the specified position
* @param ux the vectors x value
* @param uy the vectors y value
*/
private float nominator(Position pos, float ux, float uy) {
final float dx = pos.getX() - from().getX();
final float dy = pos.getY() - from().getY();
return dx * uy - dy * ux;
}
/**
* Checks whether the (ux,uy) ray starting at pos hits (or almost hits) target.
*
* @param pos the specified start position
* @param ux the rays x value
* @param uy the rays y value
* @param target the specified target position
*/
private float isCandidate(Position pos, float ux, float uy, Position target) {
final float lambda = lambda(pos, target, ux, uy);
if (lambda < -FLT_EPSILON) return -1f;
final float dx = target.getX() - pos.getX() - lambda * ux;
final float dy = target.getY() - pos.getY() - lambda * uy;
return dx * dx + dy * dy;
}
private float lambda(Position p1, Position p2, float ux, float uy) {
return ux * (p2.getX() - p1.getX()) + uy * (p2.getY() - p1.getY());
}
/**
* Returns the quotient q such that the specified point projected onto this
* segment is at q*from()+(1-q)*to().
*
* @param pos the specified points position
*/
default float project(Position pos) {
return ((pos.getX() - from().getX()) * diffX() + (pos.getY() - from().getY()) * diffY()) / lengthSquared();
}
/**
* Returns the interval of quotients between leftAngle and rightAngle
* looking from the specified position. The starting point of the
* segment must be left of its end point when looking from the
* specified position.
*
* @param pos the specified position to look from
* @param leftAngle the specified left angle
* @param rightAngle the specified right angle
*/
default Interval interval(Position pos, Angle leftAngle, Angle rightAngle) {
final float nomLeft = nominator(pos, leftAngle.x, leftAngle.y);
final float detLeft = determinant(leftAngle.x, leftAngle.y);
final float nomRight = nominator(pos, rightAngle.x, rightAngle.y);
final float detRight = determinant(rightAngle.x, rightAngle.y);
if (abs(detLeft) <= FLT_EPSILON || abs(detRight) <= FLT_EPSILON)
return new Interval(0f, 0f);
final float q1 = nomLeft / detLeft;
final float q2 = nomRight / detRight;
if (q1 > q2)
return new Interval(0f, 0f);
final float lower = q1 < ZERO_TOLERANCE ? 0f : min(1f, q1);
final float upper = q2 > 1f - ZERO_TOLERANCE ? 1f : max(0f, q2);
return new Interval(lower, upper);
}
/**
* Returns the x-coordinate of the vector from the start to the end point.
*/
default float diffX() {
return to().getX() - from().getX();
}
/**
* Returns the y-coordinate of the vector from the start to the end point.
*/
default float diffY() {
return to().getY() - from().getY();
}
/**
* Computes the determinant of the matrix whose first column vector is this segment and the
* second column vector is the specified segment.
*
* @param other the specified segment
*/
default float determinantWith(SegmentLike other) {
return diffX() * other.diffY() - diffY() * other.diffX();
}
/**
* Computes the square of the minimal distance between this and the specified segment.
*
* @param other other segment
* @return squared distance
*/
default float minDistanceSquared(SegmentLike other) {
final float rx = other.from().getX() - from().getX();
final float ry = other.from().getY() - from().getY();
final float ux = diffX();
final float uy = diffY();
final float vx = other.diffX();
final float vy = other.diffY();
final float ru = rx * ux + ry * uy;
final float rv = rx * vx + ry * vy;
final float uu = ux * ux + uy * uy;
final float uv = ux * vx + uy * vy;
final float vv = vx * vx + vy * vy;
if (uu < ZERO_TOLERANCE) { // this segment is in fact a single point
if (vv < ZERO_TOLERANCE) // other is a point, too
return rx * rx + ry * ry;
else
return sqr(other.distanceTo(from()));
}
if (vv < ZERO_TOLERANCE) // other is in fact a point
return sqr(distanceTo(other.from()));
final float det = uu * vv - uv * uv;
final float s;
final float t;
if (det < ZERO_TOLERANCE * uu * vv) {
s = min(max(ru / uu, 0f), 1f);
t = 0f;
}
else {
s = min(max((ru * vv - rv * uv) / det, 0f), 1f);
t = min(max((ru * uv - rv * uu) / det, 0f), 1f);
}
final float mu1 = min(max((t * uv + ru) / uu, 0f), 1f);
final float mu2 = min(max((s * uv - rv) / vv, 0f), 1f);
return pointAt(mu1).distanceSquaredTo(other.pointAt(mu2));
}
}

View File

@@ -0,0 +1,90 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* A class with auxiliary functions.
*/
public class Util {
private Util() { /* do not instantiate */ }
/**
* Calculates and returns the area of a polygon defined by a list of points, using the shoelace formula.
* The result is positive if the sequence of vertices is in clockwise order, and negative
* otherwise.
*
* @param points A list of positions that define the vertices of the polygon in planar coordinates.
* @return The calculated area of the polygon.
*/
public static float getArea(List<? extends Position> points) {
float sum = 0;
Position prev = getLast(points);
for (var next : points) {
sum += prev.getX() * next.getY() - next.getX() * prev.getY();
prev = next;
}
return 0.5f * sum;
}
/**
* Returns the last element of the given list.
*
* @param list the list from which to retrieve the last element
* @param <T> the type of elements in the list
* @return the last element of the list
* @throws IndexOutOfBoundsException if the list is empty
*/
public static <T> T getLast(List<T> list) {
return list.get(list.size() - 1);
}
/**
* Reverses the order of elements in the given list.
*
* @param list the list to be reversed
* @param <T> the type of elements in the list
* @return a new list with elements in reversed order
*/
public static <T> List<T> reverse(List<T> list) {
final List<T> reversed = new ArrayList<>(list);
Collections.reverse(reversed);
return reversed;
}
/**
* Creates a copy of the given list.
*
* @param list the list to be copied, may be null
* @param <T> the type of elements in the list
* @return a new list containing the elements of the original list, or null if the original list is null
*/
public static <T> List<T> copy(List<T> list) {
return list == null ? null : new ArrayList<>(list);
}
/**
* Adds an element to a set and returns a new set containing the original elements and the new element.
*
* @param set the original set, must not be null
* @param element the element to be added to the set
* @param <T> the type of elements in the set
* @param <E> the type of the element being added, must extend T
* @return a new set containing the original elements and the new element
*/
public static <T, E extends T> Set<T> add(Set<T> set, E element) {
final Set<T> newSet = new HashSet<>(set);
newSet.add(element);
return newSet;
}
}

View File

@@ -0,0 +1,286 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util.config;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.function.Function;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Abstract base class for representing configurations that can be read from properties files.
* Subclasses can define fields annotated with {@link Property} to specify the configuration keys
* and optionally {@link Separator} to specify custom separators for array values.
*/
public abstract class Config {
private static final String BLANK_SEQ = " *";
/**
* Annotation for specifying the property key for a field.
*/
@Retention(RUNTIME)
@Target(FIELD)
@Documented
protected @interface Property {
/**
* The key of the property.
*/
String value();
}
/**
* Annotation for specifying a custom separator for array values.
*/
@Retention(RUNTIME)
@Target(FIELD)
@Documented
protected @interface Separator {
/**
* The separator for array values.
*/
String value();
}
private static final Logger LOGGER = System.getLogger(Config.class.getName());
private static final Map<Class<?>, Function<String, ?>> CONVERTER_MAP = new HashMap<>();
static {
CONVERTER_MAP.put(String.class, x -> x);
CONVERTER_MAP.put(byte.class, Byte::parseByte);
CONVERTER_MAP.put(short.class, Short::parseShort);
CONVERTER_MAP.put(int.class, Integer::parseInt);
CONVERTER_MAP.put(long.class, Long::parseLong);
CONVERTER_MAP.put(boolean.class, Boolean::parseBoolean);
CONVERTER_MAP.put(float.class, Float::parseFloat);
CONVERTER_MAP.put(double.class, Double::parseDouble);
CONVERTER_MAP.put(Byte.class, Byte::parseByte);
CONVERTER_MAP.put(Short.class, Short::parseShort);
CONVERTER_MAP.put(Integer.class, Integer::parseInt);
CONVERTER_MAP.put(Long.class, Long::parseLong);
CONVERTER_MAP.put(Boolean.class, Boolean::parseBoolean);
CONVERTER_MAP.put(Float.class, Float::parseFloat);
CONVERTER_MAP.put(Double.class, Double::parseDouble);
}
/**
* Reads the specified properties file and sets the values of this config using {@link #readFrom(Properties)}.
*
* @param file the properties file to read
* @throws IOException if an I/O error occurs
* @see #readFrom(Properties)
*/
public void readFrom(File file) throws IOException {
try (Reader reader = new FileReader(file)) {
final Properties properties = new Properties();
properties.load(reader);
readFrom(properties);
}
}
/**
* Sets the values of fields annotated with {@link Property} using the specified properties.
* Array fields can be split into components using the default separator (",") or a custom separator
* specified with {@link Separator}.
*
* @param props the properties to read from
*/
public void readFrom(Properties props) {
for (Class<?> clazz = getClass(); Config.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) {
for (Field field : clazz.getDeclaredFields()) {
final Property keyAnnot = field.getAnnotation(Property.class);
if (keyAnnot != null && props.containsKey(keyAnnot.value())) {
try {
final String text = props.getProperty(keyAnnot.value());
final Object value = createValue(text, field);
setField(field, value);
}
catch (IllegalAccessException ex) {
LOGGER.log(Level.ERROR, "Cannot access " + field, ex); //NON-NLS
}
}
}
}
}
/**
* Reads the specified properties file and sets the values of this config if the file exists,
* otherwise uses default values. This method is a convenience version of {@link #readFrom(File)}
* that checks the existence of the specified file and does nothing if the file does not exist.
*
* @param file the properties file to read, if it exists
*/
public void readFromIfExists(File file) {
if (!file.exists()) {
LOGGER.log(Level.INFO, "There is no config file {0}; using default configuration", //NON-NLS
file.getAbsolutePath());
return;
}
try {
readFrom(file);
LOGGER.log(Level.INFO, "Successfully read config from {0}", file.getAbsolutePath()); //NON-NLS
}
catch (IOException e) {
LOGGER.log(Level.WARNING, "Cannot read config file " + file.getAbsolutePath(), e); //NON-NLS
}
}
/**
* Converts a string value from the properties file into an object that can be assigned to the specified field.
* For array fields, the string value is first split into components.
*
* @param value the string value to convert
* @param field the field to set with the converted value
* @return an object of the appropriate type for the field
*/
private Object createValue(String value, Field field) {
if (!field.getType().isArray())
return convertToType(value, field.getType());
// the field is an array
final Separator sepAnn = field.getDeclaredAnnotation(Separator.class);
final String sep = sepAnn == null ? "," : sepAnn.value();
final String[] split = value.split(BLANK_SEQ + sep + BLANK_SEQ, -1);
final Object array = Array.newInstance(field.getType().componentType(), split.length);
for (int i = 0; i < split.length; i++)
Array.set(array, i, convertToType(split[i], field.getType().componentType()));
return array;
}
/**
* Converts a string value into an object of the specified type.
*
* @param value the string value to convert
* @param targetType the target type to convert to
* @return an object of the specified type
*/
protected Object convertToType(String value, Class<?> targetType) {
Function<String, ?> handler = CONVERTER_MAP.get(targetType);
if (handler != null)
return handler.apply(value);
throw new IllegalArgumentException("Cannot translate " + value + " to " + targetType);
}
/**
* Returns a string representation of the configuration object, including all properties and their values.
*
* @return a string representation of the configuration object
*/
@Override
public String toString() {
final List<String> propertyStrings = getPropertyStrings();
propertyStrings.sort(String.CASE_INSENSITIVE_ORDER);
return "[\n" + String.join(",\n", propertyStrings) + "\n]";
}
/**
* Retrieves all property strings of the configuration object.
*
* @return a list of property strings
*/
private List<String> getPropertyStrings() {
final List<String> propertyStrings = new ArrayList<>();
for (Class<?> clazz = getClass(); Config.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) {
for (Field field : clazz.getDeclaredFields()) {
final String stringRepresentation = getStringRepresentation(field);
if (stringRepresentation != null)
propertyStrings.add(stringRepresentation);
}
}
return propertyStrings;
}
/**
* Retrieves the string representation of a field annotated with {@link Property}.
*
* @param field the field to retrieve the string representation for
* @return the string representation of the field, or null if the field is not annotated with {@link Property}
*/
private String getStringRepresentation(Field field) {
final Property keyAnnotation = field.getAnnotation(Property.class);
if (keyAnnotation != null) {
try {
final Object fieldValue = getField(field);
final String valueString = asString(fieldValue);
return keyAnnotation.value() + " -> " + field.getName() + " = " + valueString;
}
catch (IllegalAccessException e) {
LOGGER.log(Level.ERROR, "Cannot access " + field, e); //NON-NLS
}
}
return null;
}
/**
* Converts an object to its string representation. For arrays, string representations of their components are produced.
*
* @param value the object to convert
* @return the string representation of the object
*/
private String asString(Object value) {
if (value == null)
return "null"; //NON-NLS
if (!value.getClass().isArray())
return value.toString();
final int length = Array.getLength(value);
final List<String> components = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
final Object component = Array.get(value, i);
components.add(asString(component));
}
return "{" + String.join(", ", components) + "}";
}
/**
* Sets the value of a field, making it accessible if necessary.
*
* @param field the field to set
* @param value the value to set
* @throws IllegalAccessException if the field cannot be accessed
*/
private void setField(Field field, Object value) throws IllegalAccessException {
boolean inaccessible = !field.canAccess(this);
if (inaccessible)
field.setAccessible(true);
field.set(this, value);
if (inaccessible)
field.setAccessible(false);
LOGGER.log(Level.TRACE, "Set {0} to {1}", field, value); //NON-NLS
}
/**
* Retrieves the value of a field, making it accessible if necessary.
*
* @param field the field to retrieve the value from
* @return the value of the field
* @throws IllegalAccessException if the field cannot be accessed
*/
private Object getField(Field field) throws IllegalAccessException {
boolean inaccessible = !field.canAccess(this);
if (inaccessible)
field.setAccessible(true);
final Object value = field.get(this);
if (inaccessible)
field.setAccessible(false);
return value;
}
}

View File

@@ -0,0 +1,109 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util;
import org.junit.Test;
import static java.lang.Math.min;
import static org.junit.Assert.assertEquals;
import static pp.util.FloatMath.DEG_TO_RAD;
import static pp.util.FloatMath.ZERO_TOLERANCE;
import static pp.util.FloatMath.cos;
import static pp.util.FloatMath.sin;
public class AngleTest {
@Test
public void compareAngles() {
for (int i = 0; i < 360; i++) {
final Angle u = Angle.fromDegrees(i);
for (int j = 0; j < 360; j++) {
final Angle v = Angle.fromDegrees(j);
assertEquals("compare " + i + "° and " + j + "°", Integer.compare(i, j), (Object) u.compareTo(v));
}
}
}
@Test
public void addAngles() {
for (int i = 0; i < 360; i++) {
final Angle u = Angle.fromDegrees(i);
for (int j = 0; j < 360; j++) {
final Angle v = Angle.fromDegrees(j);
final Angle sum = u.plus(v);
assertEquals(i + "° + " + j + "°, x coordinate", cos((i + j) * DEG_TO_RAD), sum.x, ZERO_TOLERANCE);
assertEquals(i + "° + " + j + "°, y coordinate", sin((i + j) * DEG_TO_RAD), sum.y, ZERO_TOLERANCE);
}
}
}
@Test
public void subtractAngles() {
for (int i = 0; i < 360; i++) {
final Angle u = Angle.fromDegrees(i);
for (int j = 0; j < 360; j++) {
final Angle v = Angle.fromDegrees(j);
final Angle diff = u.minus(v);
assertEquals(i + "° - " + j + "°, x coordinate", cos((i - j) * DEG_TO_RAD), diff.x, ZERO_TOLERANCE);
assertEquals(i + "° - " + j + "°, y coordinate", sin((i - j) * DEG_TO_RAD), diff.y, ZERO_TOLERANCE);
}
}
}
@Test
public void minAngle() {
for (int i = 0; i < 360; i++) {
final Angle u = Angle.fromDegrees(i);
for (int j = 0; j < 360; j++) {
final Angle v = Angle.fromDegrees(j);
final Angle diff = Angle.min(u, v);
assertEquals(i + "° - " + j + "°, x coordinate", cos(min(i, j) * DEG_TO_RAD), diff.x, ZERO_TOLERANCE);
assertEquals(i + "° - " + j + "°, y coordinate", sin(min(i, j) * DEG_TO_RAD), diff.y, ZERO_TOLERANCE);
}
}
}
@Test
public void bisector() {
for (int right = 0; right < 360; right++) {
final Angle rightAngle = Angle.fromDegrees(right);
for (int add = 0; add < 360; add++) {
final int left = right + add;
final Angle bisector = Angle.fromDegrees(left).bisector(rightAngle);
final float exp = (right + 0.5f * add) * DEG_TO_RAD;
assertEquals("left=" + left + "° / right=" + right + "°, x coordinate", cos(exp), bisector.x, ZERO_TOLERANCE);
assertEquals("left=" + left + "° / right=" + right + "°, y coordinate", sin(exp), bisector.y, ZERO_TOLERANCE);
}
}
}
@Test
public void leftOf() {
for (int right = 0; right < 360; right++) {
final Angle rightAngle = Angle.fromDegrees(right);
for (int add = 1; add < 360; add++)
if (add != 180) {
final int left = right + add;
final Angle leftAngle = Angle.fromDegrees(left);
assertEquals(left + "° left of " + right + "°", add < 180, leftAngle.leftOf(rightAngle));
}
}
}
@Test
public void rightOf() {
for (int right = 0; right < 360; right++) {
final Angle rightAngle = Angle.fromDegrees(right);
for (int add = 1; add < 360; add++)
if (add != 180) {
final int left = right + add;
final Angle leftAngle = Angle.fromDegrees(left);
assertEquals(left + "° right of " + right + "°", add > 180, leftAngle.rightOf(rightAngle));
}
}
}
}

View File

@@ -0,0 +1,39 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static pp.util.FloatMath.ZERO_TOLERANCE;
public class IntervalTest {
private Interval interval;
@Before
public void setUp() {
interval = new Interval(0f, 1f);
}
@Test
public void contains() {
assertTrue(interval.contains(0.5f));
assertTrue(interval.contains(0f));
assertTrue(interval.contains(1f));
assertFalse(interval.contains(1.5f));
assertFalse(interval.contains(-0.5f));
}
@Test
public void matches() {
assertTrue(interval.matches(new Interval(0f, 1f), ZERO_TOLERANCE));
assertFalse(interval.matches(new Interval(0f, 0.99f), ZERO_TOLERANCE));
}
}

View File

@@ -0,0 +1,36 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util;
import org.junit.Test;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class RandomPositionIteratorTest {
public static final int WIDTH = 15;
public static final int HEIGHT = 25;
@Test
public void permutation() {
for (int i = 0; i < 10; i++) {
final List<Position> permutation = new ArrayList<>();
RandomPositionIterator.floatPoints(WIDTH, HEIGHT).forEachRemaining(permutation::add);
assertEquals(WIDTH * HEIGHT, permutation.size());
assertEquals(permutation.size(), new HashSet<>(permutation).size());
for (Position w : permutation) {
assertTrue(w.toString(), 0 <= w.getX() && w.getX() < WIDTH);
assertTrue(w.toString(), 0 <= w.getY() && w.getY() < HEIGHT);
}
}
}
}

View File

@@ -0,0 +1,201 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static pp.util.FloatMath.FLT_EPSILON;
import static pp.util.FloatMath.PI;
import static pp.util.FloatMath.ZERO_TOLERANCE;
import static pp.util.FloatMath.cos;
import static pp.util.FloatMath.sqr;
import static pp.util.FloatMath.sqrt;
import static pp.util.FloatMath.tan;
public class SegmentTest {
private static final float SQRT2 = sqrt(2f);
private static final Segment segment1 = new Segment(new FloatPoint(1f, -1f), new FloatPoint(1f, 1f));
private static final Segment segment2 = new Segment(new FloatPoint(SQRT2, 0f), new FloatPoint(0f, SQRT2));
private static final Segment segment3 = new Segment(new FloatPoint(2f, -1f), new FloatPoint(2f, 1f));
private static final Segment segment4 = new Segment(new FloatPoint(SQRT2, -1f), new FloatPoint(SQRT2, 1f));
private static final Segment segment5 = new Segment(new FloatPoint(-SQRT2, 2f * SQRT2), new FloatPoint(2f * SQRT2, -SQRT2));
private static final Segment segment6 = new Segment(new FloatPoint(0f, 0f), new FloatPoint(SQRT2, 1f));
private static final FloatPoint ZERO = new FloatPoint(0f, 0f);
public static final FloatPoint ONE_UP = new FloatPoint(0f, 1f);
public static final FloatPoint ONE_DOWN = new FloatPoint(0f, -1f);
@Test
public void dist1() {
assertEquals(1f, segment1.dist(ZERO, 0f), FLT_EPSILON);
assertEquals(1f, segment1.dist(ONE_UP, 0f), FLT_EPSILON);
assertEquals(1f, segment1.dist(ONE_DOWN, 0f), FLT_EPSILON);
assertEquals(1f / cos(0.125f * PI), segment1.dist(ZERO, 0.125f * PI), FLT_EPSILON);
assertEquals(1f / cos(0.125f * PI), segment1.dist(ONE_UP, 0.125f * PI), FLT_EPSILON);
assertEquals(1f / cos(0.125f * PI), segment1.dist(ONE_DOWN, 0.125f * PI), FLT_EPSILON);
assertEquals(1f / cos(0.125f * PI), segment1.dist(ZERO, -0.125f * PI), FLT_EPSILON);
assertEquals(1f / cos(0.125f * PI), segment1.dist(ONE_UP, -0.125f * PI), FLT_EPSILON);
assertEquals(1f / cos(0.125f * PI), segment1.dist(ONE_DOWN, -0.125f * PI), FLT_EPSILON);
assertEquals(SQRT2, segment1.dist(ZERO, 0.25f * PI), FLT_EPSILON);
assertEquals(SQRT2, segment1.dist(ZERO, -0.25f * PI), FLT_EPSILON);
}
@Test
public void dist2() {
assertEquals(1f, segment2.dist(ZERO, 0.25f * PI), FLT_EPSILON);
assertEquals(SQRT2, segment2.dist(ZERO, 0f), FLT_EPSILON);
assertEquals(SQRT2, segment2.dist(ZERO, 0.5f * PI), FLT_EPSILON);
assertEquals(1f / cos(0.125f * PI), segment2.dist(ZERO, 0.375f * PI), FLT_EPSILON);
assertEquals(SQRT2, segment2.dist(ZERO, PI / 2f), FLT_EPSILON);
}
@Test
public void quotient1() {
assertEquals(0.5f, segment1.quotient(ZERO, 0f), FLT_EPSILON);
assertEquals(1f, segment1.quotient(ONE_UP, 0f), FLT_EPSILON);
assertEquals(0f, segment1.quotient(ONE_DOWN, 0f), FLT_EPSILON);
assertEquals(0.5f * tan(0.125f * PI) + 0.5f, segment1.quotient(ZERO, 0.125f * PI), FLT_EPSILON);
assertEquals(0.5f * tan(0.125f * PI) + 1f, segment1.quotient(ONE_UP, 0.125f * PI), FLT_EPSILON);
assertEquals(0.5f * tan(0.125f * PI), segment1.quotient(ONE_DOWN, 0.125f * PI), FLT_EPSILON);
assertEquals(0.5f - 0.5f * tan(0.125f * PI), segment1.quotient(ZERO, -0.125f * PI), FLT_EPSILON);
assertEquals(1f - 0.5f * tan(0.125f * PI), segment1.quotient(ONE_UP, -0.125f * PI), FLT_EPSILON);
assertEquals(-0.5f * tan(0.125f * PI), segment1.quotient(ONE_DOWN, -0.125f * PI), FLT_EPSILON);
assertEquals(1f, segment1.quotient(ZERO, 0.25f * PI), FLT_EPSILON);
assertEquals(0f, segment1.quotient(ZERO, -0.25f * PI), FLT_EPSILON);
}
@Test
public void quotient2() {
assertEquals(0.5f, segment2.quotient(ZERO, 0.25f * PI), FLT_EPSILON);
assertEquals(0f, segment2.quotient(ZERO, 0f), FLT_EPSILON);
assertEquals(1f, segment2.quotient(ZERO, 0.5f * PI), FLT_EPSILON);
assertEquals(0.5f * SQRT2, segment2.quotient(ZERO, 0.375f * PI), FLT_EPSILON);
assertEquals(1f - 0.5f * SQRT2, segment2.quotient(ZERO, 0.125f * PI), FLT_EPSILON);
}
@Test
public void project() {
assertEquals(0.5f, segment1.project(ZERO), FLT_EPSILON);
assertEquals(0.5f, segment2.project(ZERO), FLT_EPSILON);
}
@Test
public void minDistanceSquared1() {
assertEquals(0f, segment1.minDistanceSquared(segment1), ZERO_TOLERANCE);
assertEquals(0f, segment1.minDistanceSquared(segment2), ZERO_TOLERANCE);
assertEquals(0f, segment2.minDistanceSquared(segment1), ZERO_TOLERANCE);
assertEquals(1f, segment1.minDistanceSquared(segment3), ZERO_TOLERANCE);
assertEquals(1f, segment3.minDistanceSquared(segment1), ZERO_TOLERANCE);
assertEquals(sqr(SQRT2 - 1f), segment1.minDistanceSquared(segment4), ZERO_TOLERANCE);
assertEquals(sqr(SQRT2 - 1f), segment4.minDistanceSquared(segment1), ZERO_TOLERANCE);
assertEquals(0f, segment1.minDistanceSquared(segment5), ZERO_TOLERANCE);
assertEquals(0f, segment5.minDistanceSquared(segment1), ZERO_TOLERANCE);
assertEquals(0f, segment1.minDistanceSquared(segment6), ZERO_TOLERANCE);
assertEquals(0f, segment6.minDistanceSquared(segment1), ZERO_TOLERANCE);
assertEquals(0f, segment2.minDistanceSquared(segment2), ZERO_TOLERANCE);
assertEquals(sqr(2f - SQRT2), segment2.minDistanceSquared(segment3), ZERO_TOLERANCE);
assertEquals(sqr(2f - SQRT2), segment3.minDistanceSquared(segment2), ZERO_TOLERANCE);
assertEquals(0f, segment2.minDistanceSquared(segment4), ZERO_TOLERANCE);
assertEquals(0f, segment4.minDistanceSquared(segment2), ZERO_TOLERANCE);
assertEquals(0f, segment2.minDistanceSquared(segment5), ZERO_TOLERANCE);
assertEquals(0f, segment5.minDistanceSquared(segment2), ZERO_TOLERANCE);
assertEquals(0f, segment2.minDistanceSquared(segment6), ZERO_TOLERANCE);
assertEquals(0f, segment6.minDistanceSquared(segment2), ZERO_TOLERANCE);
assertEquals(0f, segment3.minDistanceSquared(segment3), ZERO_TOLERANCE);
assertEquals(sqr(2f - SQRT2), segment3.minDistanceSquared(segment4), ZERO_TOLERANCE);
assertEquals(sqr(2f - SQRT2), segment4.minDistanceSquared(segment3), ZERO_TOLERANCE);
assertEquals(0f, segment3.minDistanceSquared(segment5), ZERO_TOLERANCE);
assertEquals(0f, segment5.minDistanceSquared(segment3), ZERO_TOLERANCE);
assertEquals(sqr(2f - SQRT2), segment3.minDistanceSquared(segment6), ZERO_TOLERANCE);
assertEquals(sqr(2f - SQRT2), segment6.minDistanceSquared(segment3), ZERO_TOLERANCE);
assertEquals(0f, segment4.minDistanceSquared(segment4), ZERO_TOLERANCE);
assertEquals(0f, segment4.minDistanceSquared(segment5), ZERO_TOLERANCE);
assertEquals(0f, segment5.minDistanceSquared(segment4), ZERO_TOLERANCE);
assertEquals(0f, segment4.minDistanceSquared(segment6), ZERO_TOLERANCE);
assertEquals(0f, segment6.minDistanceSquared(segment4), ZERO_TOLERANCE);
assertEquals(0f, segment5.minDistanceSquared(segment5), ZERO_TOLERANCE);
assertEquals(0f, segment5.minDistanceSquared(segment6), ZERO_TOLERANCE);
assertEquals(0f, segment6.minDistanceSquared(segment5), ZERO_TOLERANCE);
assertEquals(0f, segment6.minDistanceSquared(segment6), ZERO_TOLERANCE);
}
@Test
public void minDistanceSquared2() {
final Segment s1 = new Segment(new FloatPoint(0f, 0f), new FloatPoint(2f, 1f));
for (int i = -20; i <= 40; i++) {
final float x = i * 0.1f;
final Segment s2 = new Segment(new FloatPoint(x, 2f), new FloatPoint(x + 2f, -2f));
final float dist;
if (i <= -10)
dist = 0.8f * sqr(1f + x);
else if (i <= 15)
dist = 0f;
else
dist = 0.8f * sqr(x - 1.5f);
assertEquals("x = " + x, dist, s1.minDistanceSquared(s2), ZERO_TOLERANCE);
assertEquals("x = " + x, dist, s2.minDistanceSquared(s1), ZERO_TOLERANCE);
}
}
@Test
public void minDistanceSquared3() {
final Segment s1 = new Segment(new FloatPoint(0f, 0f), new FloatPoint(2f, 1f));
for (float i = -30; i <= 30; i++) {
final float x = i * 0.1f;
final Segment s2 = new Segment(new FloatPoint(x, 0.5f * x), new FloatPoint(x + 2f, 0.5f * x + 1f));
final float dist;
if (i <= -20)
dist = 1.25f * sqr(2f + x);
else if (i <= 20)
dist = 0f;
else
dist = 1.25f * sqr(x - 2f);
assertEquals("x = " + x, dist, s1.minDistanceSquared(s2), ZERO_TOLERANCE);
assertEquals("x = " + x, dist, s2.minDistanceSquared(s1), ZERO_TOLERANCE);
}
}
@Test
public void minDistanceSquared4() {
final Segment s1 = new Segment(new FloatPoint(0f, 0f), new FloatPoint(3f, 1.5f));
for (float i = -30; i <= 50; i++) {
final float x = i * 0.1f;
final float y = 1f - 0.5f * x;
final Segment s2 = new Segment(new FloatPoint(x, 1f), new FloatPoint(x, 1f));
final float dist;
if (i <= -5)
dist = sqr(x) + 1f;
else if (i <= 32)
dist = 0.2f * sqr(x - 2f);
else
dist = sqr(x - 3f) + 0.25f;
assertEquals("x = " + x, dist, s1.minDistanceSquared(s2), ZERO_TOLERANCE);
assertEquals("x = " + x, dist, s2.minDistanceSquared(s1), ZERO_TOLERANCE);
}
}
}

View File

@@ -0,0 +1,101 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util.config;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ConfigTest {
private TestConfig config;
@Before
public void setUp() {
config = new TestConfig();
}
@Test
public void testReadFromProperties() {
Properties properties = new Properties();
properties.setProperty("test.string", "hello"); //NON-NLS
properties.setProperty("test.int", "42");
properties.setProperty("test.boolean", "true"); //NON-NLS
properties.setProperty("test.intArray", "1; 2 ;3;4");
config.readFrom(properties);
assertEquals("hello", config.getTestString());
assertEquals(42, config.getTestInt());
assertTrue(config.isTestBoolean());
assertArrayEquals(new int[]{1, 2, 3, 4}, config.getTestIntArray());
}
@Test
public void testReadFromFile() throws IOException {
Properties properties = new Properties();
properties.setProperty("test.string", "fileTest");
properties.setProperty("test.int", "24");
properties.setProperty("test.boolean", "false"); //NON-NLS
properties.setProperty("test.intArray", "10;20;30");
File tempFile = File.createTempFile("testConfig", ".properties");
try (FileWriter writer = new FileWriter(tempFile)) {
properties.store(writer, null);
}
config.readFrom(tempFile);
assertEquals("fileTest", config.getTestString());
assertEquals(24, config.getTestInt());
assertFalse(config.isTestBoolean());
assertArrayEquals(new int[]{10, 20, 30}, config.getTestIntArray());
// Clean up
tempFile.delete();
}
@Test
public void testConvertToType() {
assertEquals(42, config.convertToType("42", int.class));
assertEquals(true, config.convertToType("true", boolean.class)); //NON-NLS
assertEquals(3.14, config.convertToType("3.14", double.class));
}
@Test(expected = IllegalArgumentException.class)
public void testConvertToTypeWithUnsupportedType() {
config.convertToType("unsupported", Object.class); //NON-NLS
}
@Test
public void testToString() {
Properties properties = new Properties();
properties.setProperty("test.string", "stringValue");
properties.setProperty("test.int", "123");
properties.setProperty("test.boolean", "true"); //NON-NLS
properties.setProperty("test.intArray", "5;6;7");
config.readFrom(properties);
String expected = "[\ntest.boolean -> testBoolean = true,\n" +
"test.int -> testInt = 123,\n" +
"test.intArray -> testIntArray = {5, 6, 7},\n" +
"test.string -> testString = stringValue\n" +
"]";
assertEquals(expected, config.toString());
}
}

View File

@@ -0,0 +1,40 @@
////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////
package pp.util.config;
class TestConfig extends Config {
@Property("test.string")
private String testString;
@Property("test.int")
private int testInt;
@Property("test.boolean")
private boolean testBoolean;
@Property("test.intArray")
@Separator(";")
private int[] testIntArray;
// Getters for testing
public String getTestString() {
return testString;
}
public int getTestInt() {
return testInt;
}
public boolean isTestBoolean() {
return testBoolean;
}
public int[] getTestIntArray() {
return testIntArray;
}
}