Si tiene un vector 2D expresado como x e y, ¿cuál es una buena manera de transformarlo en la dirección de la brújula más cercana?
p.ej
x:+1, y:+1 => NE
x:0, y:+3 => N
x:+10, y:-2 => E // closest compass direction
Si tiene un vector 2D expresado como x e y, ¿cuál es una buena manera de transformarlo en la dirección de la brújula más cercana?
p.ej
x:+1, y:+1 => NE
x:0, y:+3 => N
x:+10, y:-2 => E // closest compass direction
Respuestas:
Probablemente, la forma más simple es obtener el ángulo del vector usando atan2()
, como sugiere Tetrad en los comentarios, y luego escalarlo y redondearlo, por ejemplo (pseudocódigo):
// 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];
La octant = round( 8 * angle / (2*PI) + 8 ) % 8
línea podría necesitar alguna explicación. En casi todos los idiomas que conozco que lo tienen, la atan2()
función devuelve el ángulo en radianes. Dividiéndolo por 2 π, se convierte de radianes a fracciones de un círculo completo, y al multiplicarlo por 8, se convierte en octavos de círculo, que luego redondeamos al número entero más cercano. Finalmente, lo reducimos en el módulo 8 para encargarnos de la envoltura, de modo que tanto 0 como 8 estén correctamente asignados al este.
La razón de esto + 8
, que omití anteriormente, es que en algunos idiomas atan2()
puede arrojar resultados negativos (es decir, de - π a + π en lugar de de 0 a 2 π ) y el operador de módulo ( %
) puede definirse para devolver valores negativos para argumentos negativos (o su comportamiento para argumentos negativos puede ser indefinido). Agregando8
(es decir, un giro completo) a la entrada antes de la reducción asegura que los argumentos sean siempre positivos, sin afectar el resultado de ninguna otra manera.
Si su idioma no proporciona una función conveniente de redondear a la más cercana, puede usar una conversión de números enteros truncados y simplemente agregar 0.5 al argumento, así:
int octant = int( 8 * angle / (2*PI) + 8.5 ) % 8; // int() rounds down
Tenga en cuenta que, en algunos idiomas, la conversión flotante a entera predeterminada redondea las entradas negativas hacia arriba en lugar de hacia abajo, lo cual es otra razón para asegurarse de que la entrada siempre sea positiva.
Por supuesto, puede reemplazar todas las apariciones de 8
esa línea con algún otro número (por ejemplo, 4 o 16, o incluso 6 o 12 si está en un mapa hexadecimal) para dividir el círculo en tantas direcciones. Simplemente ajuste la enumeración / matriz en consecuencia.
atan2(y,x)
no atan2(x,y)
.
atan2(x,y)
también funcionaría si uno simplemente enumerara los encabezados de la brújula en el sentido de las agujas del reloj comenzando desde el norte.
octant = round(8 * angle / 360 + 8) % 8
quadtant = round(4 * angle / (2*PI) + 4) % 4
y el uso de enumeración: { E, N, W, S }
.
Tiene 8 opciones (o 16 o más si desea una precisión aún más fina).
Use atan2(y,x)
para obtener el ángulo de su vector.
atan2()
funciona de la siguiente manera:
Entonces x = 1, y = 0 dará como resultado 0, y es discontinuo en x = -1, y = 0, que contiene tanto π como -π.
Ahora solo necesitamos mapear la salida de atan2()
para que coincida con la de la brújula que tenemos arriba.
Probablemente, lo más sencillo de implementar es una comprobación incremental de ángulos. Aquí hay algunos pseudocódigos que se pueden modificar fácilmente para aumentar la precisión:
//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]
Ahora para agregar más precisión, simplemente agregue los valores a la dirección enum.
El algoritmo funciona verificando valores crecientes alrededor de la brújula para ver si nuestro ángulo se encuentra en algún lugar entre el último lugar donde verificamos y la nueva posición. Es por eso que comenzamos en -PI + incremento / 2. Queremos compensar nuestros controles para incluir el mismo espacio alrededor de cada dirección. Algo como esto:
West se divide en dos debido a que los valores de retorno de atan2()
en West son discontinuos.
atan2
, aunque tenga en cuenta que 0 grados probablemente sería este y no norte.
angle >=
cheques en el código anterior; por ejemplo, si el ángulo es menor que 45, el norte ya habrá sido devuelto, por lo que no es necesario verificar si el ángulo> = 45 para la verificación este. Del mismo modo, no necesita ningún cheque antes de regresar al oeste: es la única posibilidad que queda.
if
declaraciones si quieres ir por 16 direcciones o más.
Siempre que se trate de vectores, considere operaciones fundamentales de vectores en lugar de convertir a ángulos en algún marco en particular.
Dado un vector de consulta v
y un conjunto de vectores unitarios s
, el vector más alineado es el vector s_i
que maximiza dot(v,s_i)
. Esto se debe a que el producto puntual dado longitudes fijas para los parámetros tiene un máximo para vectores con la misma dirección y un mínimo para vectores con direcciones opuestas, que cambian suavemente entre ellos.
Esto se generaliza trivialmente en más dimensiones que dos, es extensible con direcciones arbitrarias y no sufre problemas específicos de cuadros como gradientes infinitos.
En cuanto a la implementación, esto se reduciría a la asociación de un vector en cada dirección cardinal con un identificador (enumeración, cadena, lo que sea necesario) que represente esa dirección. Luego, recorrerá su conjunto de direcciones, encontrando la que tenga el producto de punto más alto.
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
con float2
como la clave? Esto no parece muy serio.
Una forma que no se ha mencionado aquí es tratar los vectores como números complejos. No requieren trigonometría y pueden ser bastante intuitivos para sumar, multiplicar o redondear rotaciones, especialmente porque ya tiene sus encabezados representados como pares de números.
En caso de que no esté familiarizado con ellos, las direcciones se expresan en forma de a + b (i) con un ser el componente real yb (i) es el imaginario. Si imagina que el plano cartesiano con X siendo real e Y siendo imaginario, 1 sería este (derecha), sería norte.
Aquí está la parte clave: 8 direcciones cardinales se representan exclusivamente con los números 1, -1 o 0 para sus componentes reales e imaginarios. Entonces, todo lo que tiene que hacer es reducir sus coordenadas X, Y como una razón y redondear ambas al número entero más cercano para obtener la dirección.
NW (-1 + i) N (i) NE (1 + i)
W (-1) Origin E (1)
SW (-1 - i) S (-i) SE (1 - i)
Para la conversión de rumbo a diagonal más cercana, reduzca X e Y proporcionalmente para que el valor mayor sea exactamente 1 o -1. Conjunto
// 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));
}
Redondeando ambos componentes de lo que originalmente era (10, -2) le da 1 + 0 (i) o 1. Entonces, la dirección más cercana es este.
Lo anterior en realidad no requiere el uso de una estructura numérica compleja, pero pensar en ellos como tal hace que sea más rápido encontrar las 8 direcciones cardinales. Puede hacer matemática vectorial de la manera habitual si desea obtener el encabezado neto de dos o más vectores. (Como números complejos, no sumas, sino que multiplicas por el resultado)
Max(x, y)
debería ser Max(Abs(x, y))
trabajar para los cuadrantes negativos. Lo intenté y obtuve el mismo resultado que izb: esto cambia las direcciones de la brújula en los ángulos incorrectos. Supongo que cambiaría cuando header.y / header.x cruza 0.5 (por lo que el valor redondeado cambia de 0 a 1), que es arctan (0.5) = 26.565 °.
esto parece funcionar:
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 + signo (x)) * (1 signo (y ^ 2)) - (2 + signo (x)) * signo (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)
Cuando quieres una cadena:
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)
Esto le da constantes al utilizar campos de bits:
// 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
Una ligera mejora en el rendimiento sería colocar las <
comprobaciones en la rama else de las >
comprobaciones correspondientes , pero me abstuve de hacerlo porque perjudica la legibilidad.
if (x > 0.9) dir |= DIR_E
todo lo demás. Debería ser mejor que el código original de Phillipp y un poco más barato que usar la norma L2 y atan2. Tal vez o tal vez no.