13

There is a very handy set of 2d geometry utilities here.

The angleBetweenLines has a problem, though. The result is always positive. I need to detect both positive and negative angles, so if one line is 15 degrees "above" or "below" the other line, the shape obviously looks different.

The configuration I have is that one line remains stationary, while the other line rotates, and I need to understand what direction it is rotating in, by comparing it with the stationary line.

EDIT: in response to swestrup's comment below, the situation is actually that I have a single line, and I record its starting position. The line then rotates from its starting position, and I need to calculate the angle from its starting position to current position. E.g if it has rotated clockwise, it is positive rotation; if counterclockwise, then negative. (Or vice versa.)

How to improve the algorithm so it returns the angle as both positive or negative depending on how the lines are positioned?

Jaanus
  • 17,688
  • 15
  • 65
  • 110
  • Given two arbitrarily intersecting line segments, its hard to determine which one is 'above' the other and even which angle to measure, because they typically form an 'X' shape. Are you perhaps always using two lines with a common starting point? That makes it much more straight forward. – swestrup Apr 18 '10 at 19:27
  • Sorry, I clarified. I'm actually talking about a single line and its rotation relative to its starting position. – Jaanus Apr 18 '10 at 19:30
  • What range do you want? Do you want the full -pi to pi or are you happy with just -pi/2 to pi/2 i.e. do you care about the direction of the lines or not? – Troubadour Apr 18 '10 at 19:31
  • Don't care about direction. I only need to monitor the rotation until about 45 degrees difference. – Jaanus Apr 18 '10 at 19:32
  • @Jaanus: In that case duffymo's answer is sufficient. – Troubadour Apr 18 '10 at 19:33
  • @Troubadour: sounds cool, but I'm hoping to be lazy and get the answer written in code :P – Jaanus Apr 18 '10 at 19:38

6 Answers6

20

Here's the implementation of brainjam's suggestion. (It works with my constraints that the difference between the lines is guaranteed to be small enough that there's no need to normalize anything.)

CGFloat angleBetweenLinesInRad(CGPoint line1Start, CGPoint line1End, CGPoint line2Start, CGPoint line2End) {
    CGFloat a = line1End.x - line1Start.x;
    CGFloat b = line1End.y - line1Start.y;
    CGFloat c = line2End.x - line2Start.x;
    CGFloat d = line2End.y - line2Start.y;

    CGFloat atanA = atan2(a, b);
    CGFloat atanB = atan2(c, d);

    return atanA - atanB;
}

I like that it's concise. Would the vector version be more concise?

Pang
  • 9,564
  • 146
  • 81
  • 122
Jaanus
  • 17,688
  • 15
  • 65
  • 110
  • 1
    I accepted brainjam's answer since I really asked for the algorithm, and in general I don't like accepting my own answers especially if it's a derivative of another answer, I like to give credit. – Jaanus Jul 24 '10 at 22:23
  • +1 This is ingenious! Saved me hours worth of geometric calculations and debugging! – K Mehta Nov 04 '12 at 05:16
9

This is an easy problem involving 2D vectors. The sine of the angle between two vectors is related to the cross-product between the two vectors. And "above" or "below" is determined by the sign of the vector that's produced by the cross-product: if you cross two vectors A and B, and the cross-product produced is positive, then A is "below" B; if it's negative, A is "above" B. See Mathworld for details.

Here's how I might code it in Java:

package cruft;

import java.text.DecimalFormat;
import java.text.NumberFormat;

/**
 * VectorUtils
 * User: Michael
 * Date: Apr 18, 2010
 * Time: 4:12:45 PM
 */
public class VectorUtils
{
    private static final int DEFAULT_DIMENSIONS = 3;
    private static final NumberFormat DEFAULT_FORMAT = new DecimalFormat("0.###");

    public static void main(String[] args)
    {
        double [] a = { 1.0, 0.0, 0.0 };
        double [] b = { 0.0, 1.0, 0.0 };

        double [] c = VectorUtils.crossProduct(a, b);

        System.out.println(VectorUtils.toString(c));
    }

    public static double [] crossProduct(double [] a, double [] b)
    {
        assert ((a != null) && (a.length >= DEFAULT_DIMENSIONS ) && (b != null) && (b.length >= DEFAULT_DIMENSIONS));

        double [] c = new double[DEFAULT_DIMENSIONS];

        c[0] = +a[1]*b[2] - a[2]*b[1];
        c[1] = +a[2]*b[0] - a[0]*b[2];
        c[2] = +a[0]*b[1] - a[1]*b[0];

        return c;
    }

    public static String toString(double [] a)
    {
        StringBuilder builder = new StringBuilder(128);

        builder.append("{ ");

        for (double c : a)
        {
            builder.append(DEFAULT_FORMAT.format(c)).append(' ');
        }

        builder.append("}");

        return builder.toString();
    }
}

Check the sign of the 3rd component. If it's positive, A is "below" B; if it's negative, A is "above" B - as long as the two vectors are in the two quadrants to the right of the y-axis. Obviously, if they're both in the two quadrants to the left of the y-axis the reverse is true.

You need to think about your intuitive notions of "above" and "below". What if A is in the first quadrant (0 <= θ <= 90) and B is in the second quadrant (90 <= θ <= 180)? "Above" and "below" lose their meaning.

The line then rotates from its starting position, and I need to calculate the angle from its starting position to current position. E.g if it has rotated clockwise, it is positive rotation; if counterclockwise, then negative. (Or vice versa.)

This is exactly what the cross-product is for. The sign of the 3rd component is positive for counter-clockwise and negative for clockwise (as you look down at the plane of rotation).

Pang
  • 9,564
  • 146
  • 81
  • 122
duffymo
  • 305,152
  • 44
  • 369
  • 561
  • This sounds great, but converting vector multiplication to code is a bit beyond me, I was hoping to be lazy and get it in code form :P i.e input: what I have, coordinates of endpoints of 2 lines; output: signed angle. – Jaanus Apr 18 '10 at 19:36
  • C'mon...vector multiplication is just multiplication. Can you code a recipe? Two vectors in, one vector out. What language do you want it in? – duffymo Apr 18 '10 at 19:38
  • Wow. Thanks for putting this together. I found my atan2 version more suitable for my needs, but perhaps this will be helpful for someone else reading this. – Jaanus Apr 18 '10 at 20:32
9

@duffymo's answer is correct, but if you don't want to implement cross-product, you can use the atan2 function. This returns an angle between -π and π, and you can use it on each of the lines (or more precisely the vectors representing the lines).

If you get an angle θ for the first (stationary line), you'll have to normalize the angle φ for the second line to be between θ-π and θ+π (by adding ±2π). The angle between the two lines will then be φ-θ.

brainjam
  • 18,863
  • 8
  • 57
  • 82
  • 2
    All this vector stuff exploded my head. (Sorry, I forgot a lot of my math...) Now, I just calculate atan2 for both lines and compare these, works fine. Thanks. – Jaanus Apr 18 '10 at 20:11
1

One 'quick and dirty' method you can use is to introduce a third reference line R. So, given two lines A and B, calculate the angles between A and R and then B and R, and subtract them.

This does about twice as much calculation as is actually necessary, but is easy to explain and debug.

swestrup
  • 4,079
  • 3
  • 22
  • 33
1
// Considering two vectors CA and BA
// Computing angle from CA to BA
// Thanks to code shared by Jaanus, but atan2(y,x) is used wrongly.

float getAngleBetweenVectorsWithSignInDeg(Point2f C, Point2f A, Point2f B)
{      
    float a = A.x - C.x;
    float b = A.y - C.y;
    float c = B.x - C.x;
    float d = B.y - C.y;

    float angleA = atan2(b, a);
    float angleB = atan2(d, c);
    cout << "angleA: " << angleA << "rad, " << angleA * 180 / M_PI << " deg" << endl;
    cout << "angleB: " << angleB << "rad, " << angleB * 180 / M_PI << " deg" << endl;
    float rotationAngleRad = angleB - angleA;
    float thetaDeg = rotationAngleRad * 180.0f / M_PI;
    return thetaDeg;
}
marikhu
  • 51
  • 5
0

That function is working in RADS

There are 2pi RADS in a full circle (360 degrees)

Thus I believe the answear you are looking for is simply the returned value - 2pi

If you are asking to have that one function return both values at the same time, then you are asking to break the language, a function can only return a single value. You could pass it two pointers that it can use to set the value of so that the change can persist after the frunction ends and your program can continue to work. But not really a sensible way of solving this problem.

Edit

Just noticed that the function actually converts the Rads to Degrees as it returns the value. But the same principle will work.

thecoshman
  • 8,394
  • 8
  • 55
  • 77