Calculando a distância entre dois pontos (Latitude, Longitude)

88

Estou tentando calcular a distância entre duas posições em um mapa. Tenho armazenado em meus dados: Longitude, Latitude, X POS, Y POS.

Eu usei anteriormente o trecho abaixo.

DECLARE @orig_lat DECIMAL
DECLARE @orig_lng DECIMAL
SET @orig_lat=53.381538 set @orig_lng=-1.463526
SELECT *,
    3956 * 2 * ASIN(
          SQRT( POWER(SIN((@orig_lat - abs(dest.Latitude)) * pi()/180 / 2), 2) 
              + COS(@orig_lng * pi()/180 ) * COS(abs(dest.Latitude) * pi()/180)  
              * POWER(SIN((@orig_lng - dest.Longitude) * pi()/180 / 2), 2) )) 
          AS distance
--INTO #includeDistances
FROM #orig dest

No entanto, não confio nos dados que saem disso, parece estar dando resultados ligeiramente imprecisos.

Alguns dados de amostra, caso você precise

Latitude        Longitude     Distance 
53.429108       -2.500953     85.2981833133896

Alguém poderia me ajudar com meu código, não me importo se você quiser consertar o que já tenho se você tiver uma nova maneira de conseguir isso que seria ótimo.

Indique em que unidade de medida estão os seus resultados.

Waller
fonte
Você não deve dividir o argumento para seno pelo adicional / 2. Além disso, você pode ter mais precisão no raio da Terra, bem como usar algum Datum usado, por exemplo, pelo sistema GPS (WGS-84) que aproxima a Terra por um elipsóide (com diferentes raios no equador e nos pólos)
Aki Suihkonen
@Waller, por que você não usa o tipo Geografia / Geometria (Espacial) para conseguir isso?
Habib
3
Verifiquei seu cálculo com o Mathematica; ele pensa que a distância em milhas estatutárias (5280 pés) é 42.997, o que sugere que seu cálculo não é ligeiramente impreciso , ao contrário, é totalmente impreciso .
Marco de alto desempenho

Respostas:

126

Como está usando o SQL Server 2008, você tem o geographytipo de dados disponível, projetado exatamente para este tipo de dados:

DECLARE @source geography = 'POINT(0 51.5)'
DECLARE @target geography = 'POINT(-3 56)'

SELECT @source.STDistance(@target)

----------------------
538404.100197555

(1 row(s) affected)

Dizendo-nos que fica a cerca de 538 km de (perto) de Londres a (perto de) Edimburgo.

Naturalmente, haverá muito aprendizado a ser feito primeiro, mas uma vez que você saiba, é muito mais fácil do que implementar seu próprio cálculo de Haversine; além disso, você obtém MUITA funcionalidade.


Se quiser manter sua estrutura de dados existente, você ainda pode usar STDistance, construindo geographyinstâncias adequadas usando o Pointmétodo:

DECLARE @orig_lat DECIMAL(12, 9)
DECLARE @orig_lng DECIMAL(12, 9)
SET @orig_lat=53.381538 set @orig_lng=-1.463526

DECLARE @orig geography = geography::Point(@orig_lat, @orig_lng, 4326);

SELECT *,
    @orig.STDistance(geography::Point(dest.Latitude, dest.Longitude, 4326)) 
       AS distance
--INTO #includeDistances
FROM #orig dest
AakashM
fonte
6
@nezam não - a longitude será negativa para os locais a oeste do Meridiano
Principal
Você salvou meu dia! .. Muito obrigado!
Dhrumil Bhankhar
1
O uso da função integrada parece ser MUITO lento. Por exemplo, em um loop de 100.000 itens, leva 23 segundos em vez de 1,4 segundos para minha função definida pelo usuário (veja a resposta de Durai).
NickG
1
Só queria entrar e confirmar a recomendação de @AakashM para índices espaciais + ... para um aplicativo ETL, a diferença foi várias ordens de magnitudes melhor após a implementação dos índices espaciais
Bill Anton,
3
FYI: POINT (LONGITUDE LATITUDE) enquanto geografia :: Point (LATITUDE, LONGITUDE, 4326)
Mzn
41

A função abaixo fornece a distância entre duas geocoordenadas em milhas

create function [dbo].[fnCalcDistanceMiles] (@Lat1 decimal(8,4), @Long1 decimal(8,4), @Lat2 decimal(8,4), @Long2 decimal(8,4))
returns decimal (8,4) as
begin
declare @d decimal(28,10)
-- Convert to radians
set @Lat1 = @Lat1 / 57.2958
set @Long1 = @Long1 / 57.2958
set @Lat2 = @Lat2 / 57.2958
set @Long2 = @Long2 / 57.2958
-- Calc distance
set @d = (Sin(@Lat1) * Sin(@Lat2)) + (Cos(@Lat1) * Cos(@Lat2) * Cos(@Long2 - @Long1))
-- Convert to miles
if @d <> 0
begin
set @d = 3958.75 * Atan(Sqrt(1 - power(@d, 2)) / @d);
end
return @d
end 

A função abaixo fornece a distância entre duas geocoordenadas em quilômetros

CREATE FUNCTION dbo.fnCalcDistanceKM(@lat1 FLOAT, @lat2 FLOAT, @lon1 FLOAT, @lon2 FLOAT)
RETURNS FLOAT 
AS
BEGIN

    RETURN ACOS(SIN(PI()*@lat1/180.0)*SIN(PI()*@lat2/180.0)+COS(PI()*@lat1/180.0)*COS(PI()*@lat2/180.0)*COS(PI()*@lon2/180.0-PI()*@lon1/180.0))*6371
END

A função abaixo fornece a distância entre duas geocoordenadas em quilômetros usando o tipo de dados Geografia que foi introduzido no sql server 2008

DECLARE @g geography;
DECLARE @h geography;
SET @g = geography::STGeomFromText('LINESTRING(-122.360 47.656, -122.343 47.656)', 4326);
SET @h = geography::STGeomFromText('POINT(-122.34900 47.65100)', 4326);
SELECT @g.STDistance(@h);

Uso:

select [dbo].[fnCalcDistanceKM](13.077085,80.262675,13.065701,80.258916)

Referência: Ref1 , Ref2

Durai Amuthan.H
fonte
2
Eu precisava fazer o cálculo da distância para códigos postais de 35K em relação aos códigos postais de vários eventos ordenados por distância para um código postal. Era uma lista de coordenadas muito grande para fazer cálculos usando o tipo de dados geográficos. Quando mudei para usar a solução baseada em funções trigonométricas de linha única acima, ela funcionou muito mais rápido. Portanto, usar tipos de geografia apenas para calcular a distância parece caro. Cuidado, comprador.
Tombala
Muito útil para calcular distâncias na cláusula WHERE de uma consulta. Eu tive que envolver um ABS () em torno da expressão "set @ d =", porque encontrei alguns casos em que a função retornou uma distância negativa.
Joe Irby
1
A função para "distância entre duas geocoordenadas em quilômetros" falha se compararmos 2 pontos iguais, ela fornece o erro "Ocorreu uma operação de ponto flutuante inválida"
RRM
2
Isso foi ótimo, mas não funciona para distâncias curtas porque "decimal (8,4)" não fornece precisão suficiente.
influente de
1
@influent está correto, isso não é útil para distâncias curtas (5 milhas no meu caso)
Roger
15

Parece que a Microsoft invadiu os cérebros de todos os outros entrevistados e os fez escrever as soluções mais complicadas possíveis. Esta é a maneira mais simples, sem nenhuma função adicional / declarações de declaração:

SELECT geography::Point(LATITUDE_1, LONGITUDE_1, 4326).STDistance(geography::Point(LATITUDE_2, LONGITUDE_2, 4326))

Basta substituir os seus dados, em vez de LATITUDE_1, LONGITUDE_1, LATITUDE_2, LONGITUDE_2por exemplo:

SELECT geography::Point(53.429108, -2.500953, 4326).STDistance(geography::Point(c.Latitude, c.Longitude, 4326))
from coordinates c
Stalinko
fonte
1
para referência: STDistance () retorna distâncias na unidade linear de medida do sistema de referência espacial no qual seus dados geográficos são definidos. Você está usando SRID 4326, o que significa que STDistance () retorna distâncias em metros.
Bryan Stump
4

Como você está usando o SQL 2008 ou posterior, recomendo verificar o tipo de dados GEOGRAPHY . O SQL possui suporte integrado para consultas geoespaciais.

por exemplo, você teria uma coluna em sua tabela do tipo GEOGRAPHY que seria preenchida com uma representação geoespacial das coordenadas (verifique a referência do MSDN no link acima para obter exemplos). Este tipo de dados então expõe métodos que permitem que você execute uma série de consultas geoespaciais (por exemplo, encontrar a distância entre 2 pontos)

AdaTheDev
fonte
Só para adicionar, tentei o tipo de campo geografia, mas descobri que usar a função de Durai (usando valores de longitude e latitude diretamente) é muito mais rápido. Veja meu exemplo aqui: stackoverflow.com/a/37326089/391605
Mike Gledhill
4
Create Function [dbo].[DistanceKM] 
( 
      @Lat1 Float(18),  
      @Lat2 Float(18), 
      @Long1 Float(18), 
      @Long2 Float(18)
)
Returns Float(18)
AS
Begin
      Declare @R Float(8); 
      Declare @dLat Float(18); 
      Declare @dLon Float(18); 
      Declare @a Float(18); 
      Declare @c Float(18); 
      Declare @d Float(18);
      Set @R =  6367.45
            --Miles 3956.55  
            --Kilometers 6367.45 
            --Feet 20890584 
            --Meters 6367450 


      Set @dLat = Radians(@lat2 - @lat1);
      Set @dLon = Radians(@long2 - @long1);
      Set @a = Sin(@dLat / 2)  
                 * Sin(@dLat / 2)  
                 + Cos(Radians(@lat1)) 
                 * Cos(Radians(@lat2))  
                 * Sin(@dLon / 2)  
                 * Sin(@dLon / 2); 
      Set @c = 2 * Asin(Min(Sqrt(@a))); 

      Set @d = @R * @c; 
      Return @d; 

End
GO

Uso:

selecione dbo.DistanceKM (37.848832506474, 37.848732506474, 27.83935546875, 27.83905546875)

Saídas:

0,02849639

Você pode alterar o parâmetro @R com flutuações comentadas.

Fatih K.
fonte
Funciona perfeitamente
Tejasvi Hegde
1

Além das respostas anteriores, aqui está uma maneira de calcular a distância dentro de um SELECT:

CREATE FUNCTION Get_Distance
(   
    @La1 float , @Lo1 float , @La2 float, @Lo2 float
)
RETURNS TABLE 
AS
RETURN 
    -- Distance in Meters
    SELECT GEOGRAPHY::Point(@La1, @Lo1, 4326).STDistance(GEOGRAPHY::Point(@La2, @Lo2, 4326))
    AS Distance
GO

Uso:

select Distance
from Place P1,
     Place P2,
outer apply dbo.Get_Distance(P1.latitude, P1.longitude, P2.latitude, P2.longitude)

As funções escalares também funcionam, mas são muito ineficientes ao computar uma grande quantidade de dados.

Espero que isso possa ajudar alguém.

Thurfir
fonte