Nếu bạn có một vectơ 2D được biểu thị là x và y, cách tốt nhất để chuyển đổi nó thành hướng la bàn gần nhất là gì?
ví dụ
x:+1, y:+1 => NE
x:0, y:+3 => N
x:+10, y:-2 => E // closest compass direction
Nếu bạn có một vectơ 2D được biểu thị là x và y, cách tốt nhất để chuyển đổi nó thành hướng la bàn gần nhất là gì?
ví dụ
x:+1, y:+1 => NE
x:0, y:+3 => N
x:+10, y:-2 => E // closest compass direction
Câu trả lời:
Cách đơn giản nhất có lẽ là lấy góc của vectơ bằng cách sử dụng atan2()
, như Tetrad gợi ý trong các bình luận, sau đó chia tỷ lệ và làm tròn nó, ví dụ (mã giả):
// 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];
Các octant = round( 8 * angle / (2*PI) + 8 ) % 8
dòng có thể cần một số lời giải thích. Trong khá nhiều tất cả các ngôn ngữ mà tôi biết rằng có nó, các atan2()
chức năng trả về góc theo radian. Chia nó cho 2 π chuyển đổi nó từ radian thành các phân số của một vòng tròn đầy đủ và nhân với 8 sau đó chuyển đổi nó thành tám phần tư của một vòng tròn, sau đó chúng ta làm tròn thành số nguyên gần nhất. Cuối cùng, chúng tôi giảm modulo 8 để chăm sóc phần bao quanh, sao cho cả 0 và 8 được ánh xạ chính xác về phía đông.
Lý do + 8
, mà tôi đã bỏ qua ở trên, là trong một số ngôn ngữ atan2()
có thể trả về kết quả âm (nghĩa là từ - π đến + π thay vì từ 0 đến 2 π ) và toán tử modulo ( %
) có thể được xác định để trả về giá trị âm cho đối số phủ định (hoặc hành vi của nó đối với đối số phủ định có thể không được xác định). Việc thêm 8
(tức là một lượt đầy đủ) vào đầu vào trước khi giảm đảm bảo rằng các đối số luôn dương, không ảnh hưởng đến kết quả theo bất kỳ cách nào khác.
Nếu ngôn ngữ của bạn không xảy ra để cung cấp một hàm tròn gần nhất thuận tiện, bạn có thể sử dụng một chuyển đổi số nguyên cắt ngắn thay vào đó và chỉ cần thêm 0,5 vào đối số, như sau:
int octant = int( 8 * angle / (2*PI) + 8.5 ) % 8; // int() rounds down
Lưu ý rằng, trong một số ngôn ngữ, chuyển đổi float sang số nguyên mặc định làm tròn các đầu vào âm lên về 0 thay vì xuống, đó là một lý do khác để đảm bảo rằng đầu vào luôn dương.
Tất nhiên, bạn có thể thay thế tất cả các lần xuất hiện 8
trên dòng đó bằng một số số khác (ví dụ 4 hoặc 16 hoặc thậm chí 6 hoặc 12 nếu bạn đang ở trên bản đồ hex) để chia vòng tròn thành nhiều hướng. Chỉ cần điều chỉnh enum / mảng cho phù hợp.
atan2(y,x)
, không atan2(x,y)
.
atan2(x,y)
cũng sẽ hoạt động, nếu người ta chỉ liệt kê các tiêu đề la bàn theo thứ tự theo chiều kim đồng hồ bắt đầu từ phía bắc.
octant = round(8 * angle / 360 + 8) % 8
quadtant = round(4 * angle / (2*PI) + 4) % 4
và sử dụng enum : { E, N, W, S }
.
Bạn có 8 tùy chọn (hoặc 16 trở lên nếu bạn muốn độ chính xác cao hơn nữa).
Sử dụng atan2(y,x)
để có được góc cho vector của bạn.
atan2()
hoạt động theo cách sau:
Vì vậy, x = 1, y = 0 sẽ dẫn đến 0 và không liên tục tại x = -1, y = 0, chứa cả π và -π.
Bây giờ chúng ta chỉ cần ánh xạ đầu ra của atan2()
khớp với la bàn mà chúng ta có ở trên.
Có khả năng đơn giản nhất để thực hiện là kiểm tra các góc tăng dần. Đây là một số mã giả dễ dàng được sửa đổi để tăng độ chính xác:
//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]
Bây giờ để thêm độ chính xác, chỉ cần thêm các giá trị cho enum hướng.
Thuật toán hoạt động bằng cách kiểm tra các giá trị tăng xung quanh la bàn để xem góc của chúng ta có nằm ở đâu đó giữa nơi chúng ta kiểm tra lần cuối và vị trí mới hay không. Đó là lý do tại sao chúng tôi bắt đầu với mức tăng -PI + / 2. Chúng tôi muốn bù đắp séc của chúng tôi để bao gồm không gian bằng nhau xung quanh mỗi hướng. Một cái gì đó như thế này:
Tây bị phá vỡ làm hai vì các giá trị trả về của atan2()
West không liên tục.
atan2
, mặc dù hãy nhớ rằng 0 độ có thể sẽ ở phía đông chứ không phải phía bắc.
angle >=
kiểm tra trong mã ở trên; ví dụ: nếu góc nhỏ hơn 45 thì phía bắc sẽ được trả về, vì vậy bạn không cần kiểm tra xem góc> = 45 đối với kiểm tra phía đông. Tương tự như vậy, bạn không cần bất kỳ kiểm tra nào trước khi quay về hướng tây - đó là khả năng duy nhất còn lại.
if
báo cáo nếu bạn muốn đi từ 16 hướng trở lên.
Bất cứ khi nào bạn đang xử lý các vectơ, hãy xem xét các hoạt động vectơ cơ bản thay vì chuyển đổi thành các góc trong một số khung cụ thể.
Cho một vectơ truy vấn v
và một tập các vectơ đơn vị s
, vectơ được căn chỉnh nhiều nhất là vectơ s_i
tối đa hóa dot(v,s_i)
. Điều này là do sản phẩm chấm được cung cấp độ dài cố định cho các tham số có mức tối đa cho các vectơ có cùng hướng và tối thiểu cho vectơ có hướng ngược nhau, thay đổi giữa các điểm một cách trơn tru.
Điều này khái quát hóa tầm thường thành nhiều chiều hơn hai chiều, có thể mở rộng với các hướng tùy ý và không gặp phải các vấn đề cụ thể về khung như độ dốc vô hạn.
Thực hiện theo cách khôn ngoan, điều này sẽ giúp bạn liên kết từ một vectơ theo từng hướng chính với một mã định danh (enum, chuỗi, bất cứ thứ gì bạn cần) đại diện cho hướng đó. Sau đó, bạn sẽ lặp qua bộ chỉ đường của mình, tìm hướng có sản phẩm chấm cao nhất.
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
với float2
làm chìa khóa? Điều này không có vẻ rất nghiêm trọng.
Một cách chưa được đề cập ở đây là coi các vectơ là số phức. Chúng không yêu cầu lượng giác và có thể khá trực quan để thêm, nhân hoặc làm tròn các phép quay, đặc biệt vì bạn đã có các tiêu đề của bạn được biểu thị dưới dạng các cặp số.
Trong trường hợp bạn không quen thuộc với chúng, các hướng được thể hiện dưới dạng a + b (i) với thành phần thực và b (i) là tưởng tượng. Nếu bạn tưởng tượng mặt phẳng cartesian với X là thật và Y là tưởng tượng, 1 sẽ ở phía đông (phải), tôi sẽ ở phía bắc.
Đây là phần quan trọng: 8 hướng chính được biểu diễn riêng với các số 1, -1 hoặc 0 cho các thành phần thực và ảo của chúng. Vì vậy, tất cả những gì bạn phải làm là giảm tọa độ X, Y của mình theo tỷ lệ và làm tròn cả hai thành số nguyên gần nhất để có hướng.
NW (-1 + i) N (i) NE (1 + i)
W (-1) Origin E (1)
SW (-1 - i) S (-i) SE (1 - i)
Để chuyển đổi đường chéo từ đầu đến gần nhất, hãy giảm cả X và Y theo tỷ lệ để giá trị lớn hơn chính xác là 1 hoặc -1. Bộ
// 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));
}
Làm tròn cả hai thành phần của những gì ban đầu (10, -2) mang lại cho bạn 1 + 0 (i) hoặc 1. Vì vậy, hướng gần nhất là hướng đông.
Ở trên không thực sự yêu cầu sử dụng cấu trúc số phức, nhưng nghĩ về chúng như vậy sẽ giúp tìm ra 8 hướng chính nhanh hơn. Bạn có thể làm toán vectơ theo cách thông thường nếu bạn muốn lấy tiêu đề thuần của hai hoặc nhiều vectơ. (Là số phức, bạn không thêm, nhưng nhân cho kết quả)
Max(x, y)
nên Max(Abs(x, y))
làm việc cho các góc phần tư âm. Tôi đã thử nó và nhận được kết quả tương tự như izb - điều này chuyển hướng la bàn ở các góc sai. Tôi đoán nó sẽ chuyển đổi khi title.y / title.x vượt qua 0,5 (vì vậy giá trị làm tròn chuyển từ 0 sang 1), đó là arctan (0,5) = 26,565 °.
điều này dường như hoạt động:
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, Tây Bắc = 3, W = 4, SW = 5, S = 6, SE = 7
f (x, y) = mod ((4-2 * (1 + dấu (x)) * (1 ký (y ^ 2)) - (2 + dấu (x)) * dấu (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)
Khi bạn muốn một chuỗi:
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)
Điều này cung cấp cho bạn hằng số bằng cách sử dụng bitfield:
// 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
Một cải tiến hiệu suất nhỏ sẽ là đưa các <
-check vào nhánh khác của tương ứng>
-check , nhưng tôi đã kiềm chế không làm điều đó vì nó gây hại cho khả năng đọc.
if (x > 0.9) dir |= DIR_E
và tất cả phần còn lại. Nó phải tốt hơn mã gốc của Phillipp và rẻ hơn một chút so với sử dụng định mức L2 và atan2. Có lẽ .. hoặc có thể không.