Если у вас есть двумерный вектор, выраженный как x и y, каков хороший способ преобразовать это в ближайшее направление компаса?
например
x:+1, y:+1 => NE
x:0, y:+3 => N
x:+10, y:-2 => E // closest compass direction
Если у вас есть двумерный вектор, выраженный как x и y, каков хороший способ преобразовать это в ближайшее направление компаса?
например
x:+1, y:+1 => NE
x:0, y:+3 => N
x:+10, y:-2 => E // closest compass direction
Ответы:
Самый простой способ, вероятно, состоит в том, чтобы получить угол вектора, используя atan2()
, как предлагает Тетрад в комментариях, а затем масштабировать и округлить его, например, (псевдокод):
// 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];
octant = round( 8 * angle / (2*PI) + 8 ) % 8
Линии , возможно , потребуется какое - то объяснение. В почти всех языках , которые я знаю , что у него, функция возвращает угол в радианах. Разделив его на 2 π, мы преобразуем его из радианов в доли полного круга, а умножив на 8, затем преобразуем его в восьмые части круга, которые затем округляем до ближайшего целого числа. Наконец, мы уменьшаем его по модулю 8, чтобы позаботиться о циклическом переходе, так что и 0, и 8 правильно отображаются на восток.atan2()
Причина + 8
, о которой я пропустил выше, состоит в том, что в некоторых языках atan2()
могут возвращаться отрицательные результаты (т. Е. От - π до + π, а не от 0 до 2 π ), и оператор modulo ( %
) может быть определен так, чтобы возвращать отрицательные значения для отрицательные аргументы (или его поведение для отрицательных аргументов может быть неопределенным). Добавление 8
(т. Е. Один полный оборот) к входу до сокращения гарантирует, что аргументы всегда положительны, не влияя на результат каким-либо другим образом.
Если ваш язык не предоставляет удобную функцию округления до ближайшего, вы можете вместо этого использовать усеченное целочисленное преобразование и просто добавить 0,5 к аргументу, например так:
int octant = int( 8 * angle / (2*PI) + 8.5 ) % 8; // int() rounds down
Обратите внимание, что в некоторых языках стандартное преобразование с плавающей точкой в целое округляет отрицательные значения до нуля, а не до нуля, что является еще одной причиной того, что входные данные всегда положительны.
Конечно, вы можете заменить все вхождения в 8
этой строке каким-либо другим числом (например, 4 или 16, или даже 6 или 12, если вы находитесь на шестнадцатеричной карте), чтобы разделить круг на такое количество направлений. Просто настройте enum / array соответственно.
atan2(y,x)
не так atan2(x,y)
.
atan2(x,y)
это также сработало бы, если бы кто-то просто перечислял заголовки компаса по часовой стрелке, начиная с севера.
octant = round(8 * angle / 360 + 8) % 8
quadtant = round(4 * angle / (2*PI) + 4) % 4
и с помощью перечисления: { E, N, W, S }
.
У вас есть 8 вариантов (или 16 или более, если вы хотите еще более высокую точность).
Используйте, atan2(y,x)
чтобы получить угол для вашего вектора.
atan2()
работает следующим образом:
Таким образом, x = 1, y = 0 приведет к 0, и оно будет прерывистым при x = -1, y = 0, содержащем как π, так и -π.
Теперь нам просто нужно отобразить выходные данные atan2()
в соответствии с компасом, который мы имеем выше.
Вероятно, наиболее простым для реализации является инкрементная проверка углов. Вот некоторый псевдокод, который легко изменить для повышения точности:
//start direction from the lowest value, in this case it's west with -π
enum direction {
west,
south,
east,
north
}
increment = (2PI)/direction.count
angle = atan2(y,x);
testangle = -PI + increment/2
index = 0
while angle > testangle
index++
if(index > direction.count - 1)
return direction[0] //roll over
testangle += increment
return direction[index]
Теперь, чтобы добавить больше точности, просто добавьте значения в перечисление direction.
Алгоритм работает, проверяя возрастающие значения вокруг компаса, чтобы увидеть, лежит ли наш угол где-то между тем, где мы в последний раз проверяли, и новой позицией. Вот почему мы начинаем с -PI + increment / 2. Мы хотим компенсировать наши проверки, чтобы включить одинаковое пространство вокруг каждого направления. Что-то вроде этого:
Запад разбит на две части, потому что возвращаемые значения atan2()
на Западе носят прерывистый характер.
atan2
, хотя имейте в виду, что 0 градусов, вероятно, будут восточными, а не северными.
angle >=
проверки в коде выше; например, если угол меньше 45, то север уже будет возвращен, поэтому вам не нужно проверять угол> = 45 для проверки на восток. Точно так же перед возвращением на запад вам вообще не нужна проверка - это единственная оставшаяся возможность.
if
утверждений, если вы хотите пойти по 16 или более направлениям.
Всякий раз, когда вы имеете дело с векторами, рассмотрите фундаментальные векторные операции вместо преобразования в углы в каком-то конкретном кадре.
Учитывая вектор запроса v
и набор единичных векторов s
, наиболее выровненный вектор - это вектор, s_i
который максимизируется dot(v,s_i)
. Это связано с тем, что произведение точек с заданной фиксированной длиной для параметров имеет максимум для векторов с одинаковым направлением и минимум для векторов с противоположными направлениями, плавно изменяющихся между ними.
Это обобщает тривиально в большее количество измерений, чем два, расширяется с произвольными направлениями и не страдает от специфических для кадра проблем, таких как бесконечные градиенты.
С точки зрения реализации, это сводится к тому, чтобы связать вектор из каждого кардинального направления с идентификатором (enum, string, что вам нужно), представляющим это направление. Затем вы переберите свой набор направлений, найдя направление с наибольшим точечным произведением.
map<float2,Direction> candidates;
candidates[float2(1,0)] = E; candidates[float2(0,1)] = N; // etc.
for each (float2 dir in candidates)
{
float goodness = dot(dir, v);
if (goodness > bestResult)
{
bestResult = goodness;
bestDir = candidates[dir];
}
}
map
с float2
ключом? Это не выглядит очень серьезно.
Один из способов, который здесь не был упомянут, - рассматривать векторы как комплексные числа. Они не требуют тригонометрии и могут быть довольно интуитивно понятными для сложения, умножения или округления вращений, тем более что заголовки уже представлены в виде пар чисел.
В случае, если вы не знакомы с ними, направления выражаются в виде a + b (i) с существенным вещественным компонентом, а b (i) является мнимым. Если вы представите декартову плоскость, где X - реальное, а Y - мнимое, 1 будет востоком (справа), я буду севером.
Вот ключевая часть: 8 основных направлений представлены исключительно числами 1, -1 или 0 для их действительных и мнимых компонентов. Поэтому все, что вам нужно сделать, это уменьшить координаты X, Y как отношение и округлить оба до ближайшего целого числа, чтобы получить направление.
NW (-1 + i) N (i) NE (1 + i)
W (-1) Origin E (1)
SW (-1 - i) S (-i) SE (1 - i)
Для преобразования диагонали от направления к ближайшему уменьшите пропорционально значения X и Y, чтобы большее значение было равно 1 или -1. Устанавливать
// Some pseudocode
enum xDir { West = -1, Center = 0, East = 1 }
enum yDir { South = -1, Center = 0, North = 1 }
xDir GetXdirection(Vector2 heading)
{
return round(heading.x / Max(heading.x, heading.y));
}
yDir GetYdirection(Vector2 heading)
{
return round(heading.y / Max(heading.x, heading.y));
}
Округление обоих компонентов того, что было изначально (10, -2), дает вам 1 + 0 (i) или 1. Таким образом, самое близкое направление - восток.
Вышесказанное на самом деле не требует использования сложной числовой структуры, но если рассматривать их как таковые, можно быстрее найти 8 основных направлений. Вы можете сделать векторную математику обычным способом, если хотите получить чистый заголовок из двух или более векторов. (Как комплексные числа, вы не добавляете, а умножаете для результата)
Max(x, y)
должен Max(Abs(x, y))
работать на отрицательные квадранты. Я попробовал это и получил тот же результат, что и izb - это переключает направления компаса под неправильными углами. Я предполагаю, что он будет переключаться, когда heading.y / heading.x пересекает 0,5 (поэтому округленное значение переключается с 0 на 1), что составляет arctan (0,5) = 26,565 °.
это похоже на работу:
public class So49290 {
int piece(int x,int y) {
double angle=Math.atan2(y,x);
if(angle<0) angle+=2*Math.PI;
int piece=(int)Math.round(n*angle/(2*Math.PI));
if(piece==n)
piece=0;
return piece;
}
void run(int x,int y) {
System.out.println("("+x+","+y+") is "+s[piece(x,y)]);
}
public static void main(String[] args) {
So49290 so=new So49290();
so.run(1,0);
so.run(1,1);
so.run(0,1);
so.run(-1,1);
so.run(-1,0);
so.run(-1,-1);
so.run(0,-1);
so.run(1,-1);
}
int n=8;
static final String[] s=new String[] {"e","ne","n","nw","w","sw","s","se"};
}
Е = 0, СВ = 1, N = 2, СЗ = 3, W = 4, SW = 5, S = 6, SE = 7
f (x, y) = mod ((4-2 * (1 + знак (x)) * * (1 знак (y ^ 2)) - (2 + знак (x)) * знак (y)
-(1+sign(abs(sign(x*y)*atan((abs(x)-abs(y))/(abs(x)+abs(y))))
-pi()/(8+10^-15)))/2*sign((x^2-y^2)*(x*y))),8)
Когда вы хотите строку:
h_axis = ""
v_axis = ""
if (x > 0) h_axis = "E"
if (x < 0) h_axis = "W"
if (y > 0) v_axis = "S"
if (y < 0) v_axis = "N"
return v_axis.append_string(h_axis)
Это дает вам константы, используя битовые поля:
// main direction constants
DIR_E = 0x1
DIR_W = 0x2
DIR_S = 0x4
DIR_N = 0x8
// mixed direction constants
DIR_NW = DIR_N | DIR_W
DIR_SW = DIR_S | DIR_W
DIR_NE = DIR_N | DIR_E
DIR_SE = DIR_S | DIR_E
// calculating the direction
dir = 0x0
if (x > 0) dir |= DIR_E
if (x < 0) dir |= DIR_W
if (y > 0) dir |= DIR_S
if (y < 0) dir |= DIR_N
return dir
Небольшое улучшение производительности может заключаться в том, чтобы поместить <
-checks в ветку else соответствующих >
-checks, но я воздержался от этого, потому что это вредит читабельности.
if (x > 0.9) dir |= DIR_E
всего остального. Это должно быть лучше, чем оригинальный код Филиппа, и немного дешевле, чем использование нормы L2 и atan2. Может быть, а может и нет.