Якщо у вас двовимірний вектор, виражений 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. Можливо .. а може й ні.