Friday, May 4, 2018

algorithm - What's the best way of transforming a 2D vector into the closest 8-way compass direction?


If you have a 2D vector expressed as x and y, what's a good way of transforming that into the closest compass direction?


e.g.


x:+1,  y:+1 => NE
x:0, y:+3 => N
x:+10, y:-2 => E // closest compass direction

Answer




The simplest way is probably to get the angle of the vector using atan2(), as Tetrad suggests in the comments, and then scale and round it, e.g. (pseudocode):


// enumerated counterclockwise, starting from east = 0:
enum compassDir {
E = 0, NE = 1,
N = 2, NW = 3,
W = 4, SW = 5,
S = 6, SE = 7
};

// for string conversion, if you can't just do e.g. dir.toString():

const string[8] headings = { "E", "NE", "N", "NW", "W", "SW", "S", "SE" };

// actual conversion code:
float angle = atan2( vector.y, vector.x );
int octant = round( 8 * angle / (2*PI) + 8 ) % 8;

compassDir dir = (compassDir) octant; // typecast to enum: 0 -> E etc.
string dirStr = headings[octant];

The octant = round( 8 * angle / (2*PI) + 8 ) % 8 line might need some explanation. In pretty much all languages that I know of that have it, the atan2() function returns the angle in radians. Dividing it by 2π converts it from radians to fractions of a full circle, and multiplying by 8 then converts it to eighths of a circle, which we then round to the nearest integer. Finally, we reduce it modulo 8 to take care of the wrap-around, so that both 0 and 8 are correctly mapped to east.



The reason for the + 8, which I skipped past above, is that in some languages atan2() may return negative results (i.e. from −π to +π rather than from 0 to 2π) and the modulo operator (%) may be defined to return negative values for negative arguments (or its behavior for negative arguments may be undefined). Adding 8 (i.e. one full turn) to the input before reduction ensures that the arguments are always positive, without affecting the result in any other way.


If your language doesn't happen to provide a convenient round-to-nearest function, you can use a truncating integer conversion instead and just add 0.5 to the argument, like this:


int octant = int( 8 * angle / (2*PI) + 8.5 ) % 8;  // int() rounds down

Note that, in some languages, the default float-to-integer conversion rounds negative inputs up towards zero rather than down, which is another reason to make sure that the input is always positive.


Of course, you can replace all occurrences of 8 on that line with some other number (e.g. 4 or 16, or even 6 or 12 if you're on a hex map) to divide the circle into that many directions. Just adjust the enum/array accordingly.


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...