Uma maneira rápida e suja usa uma subdivisão esférica recursiva . Começando com uma triangulação da superfície da Terra, divida recursivamente cada triângulo de um vértice até o meio do lado mais longo. (Idealmente, você dividirá o triângulo em duas partes de diâmetro igual ou partes de área igual, mas como essas envolvem algum cálculo complicado, eu apenas divido os lados exatamente ao meio: isso faz com que os vários triângulos acabem variando um pouco de tamanho, mas isso não parece crítico para este aplicativo.)
Obviamente, você manterá essa subdivisão em uma estrutura de dados que permite identificar rapidamente o triângulo no qual reside qualquer ponto arbitrário. Uma árvore binária (com base nas chamadas recursivas) funciona bem: cada vez que um triângulo é dividido, a árvore é dividida no nó desse triângulo. Os dados referentes ao plano de divisão são mantidos, para que você possa determinar rapidamente de que lado do avião está qualquer ponto arbitrário: que determinará se deve viajar para a esquerda ou para a direita na árvore.
(Eu disse dividir o "plano"? Sim - se modelar a superfície da Terra como uma esfera e usar coordenadas geocêntricas (x, y, z), a maioria dos nossos cálculos ocorrerá em três dimensões, onde os lados dos triângulos são pedaços de interseções da esfera com planos através de sua origem. Isso torna os cálculos rápidos e fáceis.)
Ilustrarei mostrando o procedimento em um octante de uma esfera; os outros sete octantes são processados da mesma maneira. Tal octante é um triângulo 90-90-90. Nos meus gráficos, desenharei triângulos euclidianos nos mesmos cantos: eles não parecem muito bons até ficar pequenos, mas podem ser desenhados com facilidade e rapidez. Aqui está o triângulo euclidiano correspondente ao octante: é o início do procedimento.
Como todos os lados têm o mesmo comprimento, um é escolhido aleatoriamente como o "mais longo" e subdividido:
Repita isso para cada um dos novos triângulos:
Após n etapas, teremos 2 ^ n triângulos. Aqui está a situação após 10 etapas, mostrando 1024 triângulos no octante (e 8192 na esfera geral):
Como ilustração adicional, gerei um ponto aleatório dentro deste octante e viajei pela árvore de subdivisão até o lado mais longo do triângulo atingir menos de 0,05 radianos. Os triângulos (cartesianos) são mostrados com o ponto de sonda em vermelho.
Aliás, para restringir a localização de um ponto a um grau de latitude (aproximadamente), você deve notar que isso é cerca de 1/60 radiano e, portanto, cobre cerca de (1/60) ^ 2 / (Pi / 2) = 1/6000 da superfície total. Como cada subdivisão reduz pela metade o tamanho do triângulo, cerca de 13 a 14 subdivisões do octante farão o truque. Isso não é muita computação - como veremos abaixo - tornando eficiente não armazenar a árvore, mas executar a subdivisão em tempo real. No começo, você anotaria em que ponto o ponto se encontra - que é determinado pelos sinais de suas três coordenadas, que podem ser registradas como um número binário de três dígitos - e a cada passo você deseja se lembrar se o ponto está à esquerda (0) ou à direita (1) do triângulo. Isso fornece outro número binário de 14 dígitos. Você pode usar esses códigos para agrupar pontos arbitrários.
(Geralmente, quando dois códigos estão próximos como números binários reais, os pontos correspondentes estão próximos; no entanto, os pontos ainda podem estar próximos e têm códigos notavelmente diferentes. Considere dois pontos um metro separados do Equador, por exemplo: seus códigos devem diferir mesmo antes do ponto binário, porque eles estão em diferentes octantes. Esse tipo de coisa é inevitável com qualquer particionamento fixo de espaço.)
Eu usei o Mathematica 8 para implementar isso: você pode usá- lo como está ou como pseudocódigo para uma implementação em seu ambiente de programação favorito.
Determine em que lado do plano 0-ab ponto p se encontra:
side[p_, {a_, b_}] := If[Det[{p, a, b}] >= 0, left, right];
Refine o triângulo abc com base no ponto p.
refine[p_, {a_, b_, c_}] := Block[{sides, x, y, z, m},
sides = Norm /@ {b - c, c - a, a - b} // N;
{x, y, z} = RotateLeft[{a, b, c}, First[Position[sides, Max[sides]]] - 1];
m = Normalize[Mean[{y, z}]];
If[side[p, {x, m}] === right, {y, m, x}, {x, m, z}]
]
A última figura foi desenhada exibindo o octante e, além disso, renderizando a seguinte lista como um conjunto de polígonos:
p = Normalize@RandomReal[NormalDistribution[0, 1], 3] (* Random point *)
{a, b, c} = IdentityMatrix[3] . DiagonalMatrix[Sign[p]] // N (* First octant *)
NestWhileList[refine[p, #] &, {a, b, c}, Norm[#[[1]] - #[[2]]] >= 0.05 &, 1, 16]
NestWhileList
aplica repetidamente uma operação ( refine
) enquanto uma condição pertence (o triângulo é grande) ou até que uma contagem máxima de operações seja atingida (16).
Para exibir a triangulação completa do octante, comecei com o primeiro octante e repeti o refinamento dez vezes. Isso começa com uma ligeira modificação de refine
:
split[{a_, b_, c_}] := Module[{sides, x, y, z, m},
sides = Norm /@ {b - c, c - a, a - b} // N;
{x, y, z} = RotateLeft[{a, b, c}, First[Position[sides, Max[sides]]] - 1];
m = Normalize[Mean[{y, z}]];
{{y, m, x}, {x, m, z}}
]
A diferença é que split
retorna as duas metades de seu triângulo de entrada, e não aquela em que um determinado ponto se encontra. A triangulação completa é obtida iterando isto:
triangles = NestList[Flatten[split /@ #, 1] &, {IdentityMatrix[3] // N}, 10];
Para verificar, calculei uma medida do tamanho de cada triângulo e olhei para o intervalo. (Esse "tamanho" é proporcional à figura em forma de pirâmide subtendida por cada triângulo e pelo centro da esfera; para triângulos pequenos como esse, esse tamanho é essencialmente proporcional à sua área esférica.)
Through[{Min, Max}[Map[Round[Det[#], 0.00001] &, triangles[[10]] // N, {1}]]]
{0,00523, 0,00739}
Assim, os tamanhos variam cerca de 25% em relação à sua média: isso parece razoável para se obter uma maneira aproximadamente uniforme de agrupar pontos.
Ao digitalizar esse código, você não notará trigonometria : o único local necessário, se for o caso, será a conversão entre coordenadas esféricas e cartesianas. O código também não projeta a superfície da Terra em nenhum mapa, evitando as distorções correspondentes. Caso contrário, ele usa apenas a média ( Mean
), o Teorema de Pitágoras ( Norm
) e um determinante de 3 por 3 ( Det
) para fazer todo o trabalho. (Há alguns simples comandos lista de manipulação, como RotateLeft
e Flatten
, também, juntamente com uma busca para o lado mais longo de cada triângulo.)