Якщо у вас двовимірний вектор, виражений 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 π ), і оператор модуля ( %) може бути визначений для повернення від'ємних значень для негативні аргументи (або його поведінка щодо негативних аргументів може бути невизначеною). Додавання 8(тобто один повний поворот) до вводу перед скороченням гарантує, що аргументи завжди позитивні, не впливаючи на результат будь-яким іншим способом.
Якщо у вашій мові не передбачено зручної функції "круглого до найближчого", ви можете замість цього перетворити цілочисельне перетворення і просто додати 0,5 до аргументу, як це:
int octant = int( 8 * angle / (2*PI) + 8.5 ) % 8; // int() rounds down
Зауважте, що в деяких мовах конверсія з плаваючою цілою на цілі цілі обводить негативні входи в бік до нуля, а не вниз, що є ще однією причиною впевненості, що введення завжди позитивне.
Звичайно, ви можете замінити всі входження 8на цьому рядку на якесь інше число (наприклад, 4 або 16, або навіть 6 або 12, якщо ви знаходитесь на шістнадцятковій карті), щоб розділити коло на стільки напрямків. Просто відповідно відрегулюйте enum / масив.
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]
Тепер, щоб додати більше точності, просто додайте значення до перерахунку напрямку.
Алгоритм працює, перевіряючи зростаючі значення навколо компаса, щоб побачити, чи лежить наш кут десь між тим, де ми востаннє перевіряли, і новим положенням. Ось чому ми починаємо з -PI + приріст / 2. Ми хочемо зрушити наші чеки, щоб вони містили рівний простір навколо кожного напрямку. Щось на зразок цього:

Захід розбивається надвоє, оскільки значення повернення atan2()на West припиняються.
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ключем? Це не дуже серйозно.
Один із способів, про який тут не згадувалося - трактування векторів як складних чисел. Вони не потребують тригонометрії і можуть бути досить інтуїтивно зрозумілими для додавання, множення чи округлення обертів, тим більше, що у вас уже заголовки представлені у вигляді пари чисел.
Якщо ви не знайомі з ними, вказівки виражаються у вигляді + 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 - це перемикає напрямки компаса під неправильними кутами. Я б припустив, що він переключиться, коли заголовок.y / header.x перетинає 0,5 (тому округлене значення переходить з 0 на 1), що є арктаном (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"};
}
E = 0, NE = 1, N = 2, NW = 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
Невелике поліпшення продуктивності полягатиме в тому, щоб помістити <-чеки в іншу гілку відповідних >-проверок, але я утримався від цього, оскільки це шкодить читабельності.
if (x > 0.9) dir |= DIR_Eта все інше. Це повинно бути краще, ніж оригінальний код Філіппа, і трохи дешевше, ніж використання норми L2 та atan2. Можливо .. а може й ні.