프로젝트

[개인] DLD Project (5) - 지도상 두 좌표 사이 거리 구하기 (feat. Haversine 🤔)

jaee 2023. 2. 23. 11:47

 

틀린 내용이 있을 수 있습니다.
발견하시면 말씀 부탁드립니다! 🙇


현재 내 위치에서 목적지까지의 거리를 구하여 사용자에게 보여주는 기능을 구현해 보자. Google Maps Distance Matrix API를 통해 지점 간의 거리 데이터를 가져올 수 있다고 하며, 검색해 보면 해당 API를 통해 기능을 구현한 블로그 글들도 나온다. 하지만 난 해당 API를 사용하지 않기로 했다. 왜냐하면 우리나라에서는 대중교통을 이용했을 때의 거리만 조회할 수 있도록 한정되어 있기 때문이다. 이는 이번 프로젝트의 주제와는 거리가 있기에 계산식을 통해 두 지점 간의 거리를 구하기로 했다. (사실 걷는 사람 입장에서는 대중교통을 통한 거리나 직선거리나 도긴개긴이다🥲)

 

처음에는 단순히 두 지점의 좌표를 알고 있기에 어릴 적 배웠던 피타고라스 정의(A² + B² = C²)로 계산하면 되겠지 생각했다. 그러나 거리 계산 함수를 작성한 뒤 함수 결과 값과 실제 구글 지도에서 가져온 값을 비교하니 오차가 있었다. 구글링을 통해 알아보니 지도에서 좌표 간의 거리를 구할 때는 하버사인(Haversine) 공식을 사용해 지구의 곡률까지 계산을 해야 한다고 한다.

https://en.wikipedia.org/wiki/Haversine_formula
https://en.wikipedia.org/wiki/Haversine_formula

 

하버사인 공식을 통해 두 좌표 간의 거리를 구하기 위한 최종적인 식은 아래와 같다.

https://en.wikipedia.org/wiki/Haversine_formula

 

해당 식을 통해 두 지점 간의 거리를 구하는 예시는 인터넷에 많았고, 그중 몇 개의 글을 참고해 함수를 작성했다. 참고 1 / 참고 2

const calcDistanceHaversine = (curObj, destObj) => {
  const currX = curObj['lat']; // 출발지 위도
  const currY = curObj['lng']; // 출발지 경도
  const destX = destObj['lat']; // 목적지 위도
  const destY = destObj['lng']; // 목적지 경도

  const radius = 6371; // 지구 반지름(km)
  const toRadian = Math.PI / 180;

  const deltaLat = Math.abs(currX - destX) * toRadian;
  const deltaLng = Math.abs(currY - destY) * toRadian;

  const squareSinDeltLat = Math.pow(Math.sin(deltaLat / 2), 2);
  const squareSinDeltLng = Math.pow(Math.sin(deltaLng / 2), 2);

  const squareRoot = Math.sqrt(
    squareSinDeltLat +
      Math.cos(currX * toRadian) *
        Math.cos(destX * toRadian) *
        squareSinDeltLng,
  );

  const result = 2 * radius * Math.asin(squareRoot);

  return result;
};

 

이제 위 함수를 뜯어보며 이해해보자. 아래는 8번째 줄의 코드이다.

  const toRadian = Math.PI / 180;

변수명을 통해 추측가능하듯 이건 라디안과 연관 있다. 라디안(Radian, 호도)이란 원의 원점을 중심으로 반지름 길이만큼 한 방향으로 움직였을 때 대응하는 각의 크기를 의미한다(by. 나무위키).

https://namu.wiki/w/%ED%98%B8%EB%8F%84%EB%B2%95

 

참고로 π 라디안은 180˚이다. 이해하면 좋겠지만 그냥 외우자. 즉, 원의 원점을 중심으로 π(3.14...) 길이만큼 한 방향으로 움직이면 대응하는 각이 180˚가 된다는 이야기다.

π 라디안(rad) = 180˚
1 라디안(rad)  = 180˚/ π
1˚ = π / 180

 

돌아가서, Math.PI / 180은 도(˚)를 라디안 단위로 변환할 때 사용할 값이다. 도(˚)를 라디안 단위로 변환하기 위해서는 아래 공식을 사용한다.

도(˚) * π / 180

 

이를 토대로 10~11번째 줄 코드는 다음과 같이 이해할 수 있다.

// 출발지와 목적지 위도 차를 라디안 단위로 변환
const deltaLat = Math.abs(currX - destX) * toRadian; 
// 출발지와 목적지 경도 차를 라디안 단위로 변환
const deltaLng = Math.abs(currY - destY) * toRadian;

라디안 값으로 변환함으로서 해당 값을 Math.sin(), Math.cos() 메소드들의 파라미터로 사용할 수 있게 되었다 (참고로 Math.sin()Math.cos(), 이 식에서는 사용되지 않은 Math.tan()까지 모두 라디안인 수를 파라미터로 받는다). deltaLat, deltaLng 두 개의 숫자를 Math.sin() 메소드를 통해 사인 값으로 변환한 뒤 제곱값을 구한다. 

const squareSinDeltLat = Math.pow(Math.sin(deltaLat / 2), 2);
const squareSinDeltLng = Math.pow(Math.sin(deltaLng / 2), 2);

 

이어서 공식에 나온 대로 사인 값과 코사인 값을 계산한 결과의 제곱근을 구하고, 해당 값에 sin의 역함수인 arcsin을 곱한다. 이 값에 2 * radius까지 곱해주면 지도상의 두 지점 간 거리를 구할 수 있다.

const squareRoot = Math.sqrt(
  squareSinDeltLat +
  Math.cos(currX * toRadian) * Math.cos(destX * toRadian) * squareSinDeltLng
);

const result = 2 * radius * Math.asin(squareRoot);

 

 

 


참고 자료