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