node.js

다음 map api를 이용하여 위치기반 중간 지점 찾기 프로그램

RYUHA/ editor

  • 14 comments
  • 14,893 views
  • 2017년 7월 4일

2명 이상의 사용자 위치를 기반으로 중간 지점을 찾아 경로 탐색과 더 나아가 그 주변 즐길거리를 추천해주는 앱을 만들어 봅니다.

1. 시작하기

두명 이상의 사용자의 위치를 기반으로 중간 지점을 찾아 그 주변 즐길거리를 추천해주는 프로그램을 만들어 보려고 합니다!

보통 약속 장소를 정할 때 중간 지점을 잡으려고 하지만 막상 찾아보면 잘 모르는 곳 이거나 익숙한 곳으로 잡게 되면 한명은 먼 곳일 때가 많아 고민이 됩니다.
그러한 고민을 없애기 위해 누구에게든 공평한 중간 지점을 찾아 그 주변 놀거리, 먹거리를 알려주는 프로그램을 만들어 봅시다.

간단하게, 두 사람 혹은 그 이상의 사람들(처음엔 두명 먼저 만들어 봅시당)의 위치가 지정되면 그걸 기준으로 가운데 지점을 찾아서 그 주변에 뭐가 있는지 알려주고 가는 방법 알려주는게 이 프로그램의 목표입니다.

필요한 기능이나 정보는 지도 api, 자기 위치 전송(처음은 핀으로 지정하는 걸로 시작), 중간 지점을 찾는 알고리즘, 경로 탐색의 기능이 필요합니다.

이 글은 시리즈로 진행되며 그 순서는 다음과 같습니다.
1. 주요 지도 api들을 비교하고 하나를 정해서 설정하는 법 —> api에서 사용할 수 있는 기능 살펴보기
2. 지도에서 중간 점 찾는 알고리즘 —> 핀 지정하여 좌표 탐색 후 찾는 법
3. 경로 탐색하는 법 —> 각 지점에서 중간 점으로 경로탐색하는 법 —> 두가지 동시에 보여주는 법
4. 주변에 뭐 있는지 자료 불러오기
5. DB에 저장해서 히스토리 내역 확인하는 법

위와 같은 주제로 글들을 진행할 것입니다. 그럼 이제 시작 해볼까요?

 


 

1. 지도 api 설정하기

 
여러 api 비교하여 결정해봅시다.
가장 대표적인 지도 api인 구글, 네이버, 다음 중에 하나로 정하기로 했습니다.
우선은 모바일이 아닌 웹에서 사용할 것이므로 제스쳐 이벤트는 굳이 비교하지 않아도 되고 지도의 정보량과 처리 한도 등을 위주로 살펴보았습니다.

– 구글 api : 정보량은 매우 많고 대부분의 지도 앱들이 구글맵을 사용하지만 하루 한도가 25,000회
– 네이버 api : 우리나라에서 가장 익숙한 지도 앱(본인 기준), 처리한도 : 웹 100,000/일, 모바일 5,000/일
– 다음 api : api 라이브러리 및 문서가 잘 정리되어있고 스트릿 뷰가 좋다. 지도 위에 다양한 도형을 그릴 수 있는 기능이 있는데 중심점을 찾고 시각적으로 표현할 때 좋을 것 같다. 1일 50,000회 사용 가능

간단하게 저정도의 차이가 있는데 우선 베타 버전으로 만드는 것이므로 한도도 크게 상관없고, 눈에 익숙하고 사용하기가 편한 것으로 정하기로 했습니다.
그래서 개발자 입장에서 라이브러리가 잘 정리되어있고, 사용자 입장에서도 사용이 편한 다음 api를 사용하기로 했습니다.

이제 다음 지도 api 설정하는 방법을 알아볼까요?
http://apis.map.daum.net/web/ 에 접속하여 가이드를 따라갑니다.
먼저 키 발급이 필요합니다. 위 사이트에 접속하여 ‘앱만들기’를 선택하여 자신이 만들 앱 이름을 줍니다. 그러면 왼쪽 사이드 메뉴에 앱이 생성되고 api 키 탭이 있습니다. 거기로 가서 REST/JS API를 선택합니다.

사용할 플랫폼을 선택하고(우리의 경우는 웹이므로 웹 브라우저) 아래에 지도가 사용될 url을 적어주면 API 키가 발급 됩니다.

그 후 직접 사용하기 위해선, head부분에<script src=”//apis.daum.net/maps/maps3.js?apikey=발급받은 API KEY를 넣으시면 됩니다.” type=”text/javascript”></script>를 선언 해줍니다. head든 body든 상관 없지만 실행 코드보다 항상 위에 있어야 하므로 head에 넣는걸 권장합니다.

그럼 잘 적용 되었는지 확인을 해봅시다.
node.js를 사용할 것이므로 기본 초기 설정은 튜토리얼의 ‘node.js 기본 구조 구축하기(express-generator)’를 참조하세요.
프로젝트 창을 열고 index.ejs 파일에 다음 코드를 입력 해줍니다.

 

<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
    <script type="text/javascript" src="//apis.daum.net/maps/maps3.js?apikey=부여받은api key입력"></script> 
  </head>
  <body>
    <h1><%= title %></h1>
    <p>Welcome to <%= title %></p>
    <div id="map" style="width:500px;height:400px;"></div>
    <script>
      var container = document.getElementById('map'); //지도를 담을 영역의 DOM 레퍼런스
      var options = { //지도를 생성할 때 필요한 기본 옵션
        center: new daum.maps.LatLng(33.450701, 126.570667), //지도의 중심좌표.
        level: 3 //지도의 레벨(확대, 축소 정도)
      };

      var map = new daum.maps.Map(container, options); //지도 생성 및 객체 리턴
    </script>
  </body>
</html>

그리고 실행을 시켜보면,

이렇게 나옵니다!

 

2. api 기능 살펴보기

 

그럼 api sample들을 보면서 사용할 수 있는 기능들을 살펴 봅시다.
http://apis.map.daum.net/web/sample/ 에 가면 지도를 활용할 수 있는 기본적인 샘플 들이 있는데 거기서 필요한 것들을 가져와 쓰도록 합시다.
첫번째로 필요한 기능은 pin(2개) 지정하고 그 위치의 좌표값을 얻어오는 것입니다. 부수적으로 드래그를 가능하게 하게 하고 더 나아가 사용자의 위치를 자동으로 추적하는 기능이 필요합니다.
sample 페이지에 있는 ‘드래그 가능한 마커 생성하기’, ‘여러개 마커 표시하기’, ‘인포 윈도우 생성하기’, ‘geolocation으로 마커 표시하기’가 필요해 보입니다. 하나씩 적용해서 합쳐 봅시다.
먼저 지도를 클릭하면 드래그가 가능한 마커를 생성하게 합시다.

 

<div id="map" style="width:500px;height:400px;"></div>

<script>
var container = document.getElementById('map'); //지도를 담을 영역의 DOM 레퍼런스
      var options = { //지도를 생성할 때 필요한 기본 옵션
        center: new daum.maps.LatLng(33.450701, 126.570667), //지도의 중심좌표.
        level: 3 //지도의 레벨(확대, 축소 정도)
      };

      var map = new daum.maps.Map(container, options); //지도 생성 및 객체 리턴

      var marker = new daum.maps.Marker({
        // 지도 중심좌표에 마커를 생성합니다
        position: map.getCenter()
      });
      // 지도에 마커를 표시합니다
      marker.setMap(map);

      marker.setDraggable(true); // 마커 드래그 가능하도록 설정

      // 지도에 클릭 이벤트를 등록합니다
      // 지도를 클릭하면 마지막 파라미터로 넘어온 함수를 호출합니다
      daum.maps.event.addListener(map, 'click', function(mouseEvent) {

        // 클릭한 위도, 경도 정보를 가져옵니다
        var latlng = mouseEvent.latLng;

        // 마커 위치를 클릭한 위치로 옮깁니다
        marker.setPosition(latlng);
      });
</script>

위의 코드를 실행시켜보면 지도를 클릭하면 마커가 이동하고, 마커를 드래그 하면 드래그한 지점으로 이동함을 볼 수 있습니다.

이제 여러개의 마커를 동시에 표시하는 것을 구현해 봅시다. 샘플 코드에 ‘여러 개의 마커 제어하기’를 응용하여 위의 코드와 합칩니다.

 

daum.maps.event.addListener(map, 'click', function(mouseEvent) {
        // 클릭한 위치에 마커를 추가하는 함수
        addMarker(mouseEvent.latLng);
      });

      // 지도에 표시된 마커 객체를 가지고 있을 배열입니다
      var markers = [];

      // 마커를 생성하고 지도위에 표시하는 함수입니다
      function addMarker(position) {
        // 마커를 생성합니다
        var marker = new daum.maps.Marker({
          position: position
        });

        marker.setDraggable(true); // 마커 드래그 가능하도록 설정

        // 마커가 지도 위에 표시되도록 설정합니다
        marker.setMap(map);

        // 생성된 마커를 배열에 추가합니다
        markers.push(marker);
      }

위의 코드를 실행하면 지도에 클릭하면 여러개의 마커가 생기고 각각의 마커들을 드래그가 가능해집니다.

이제 각각의 마커에 인포 윈도우를 생성해 볼까요?
클릭하면 마커의 위치를 인포 윈도우에 띄워줍니다.
addMarker 함수가 실행될 때 같이 진행되면 되므로, 함수 안에 넣어줍니다.

 

//addMarker 함수 밖에 배열을 선언 해줍니다.
var iwContents = []; // 인포 윈도우 객체를 가지고 있을 배열

function addMarker(position) {

        var iwContent =  new daum.maps.InfoWindow({ // 인포 윈도우에 들어갈 내용.
          position : position,
          content : "마커의 위치는 " + position.getLat().toFixed(4) + "," + position.getLng().toFixed(4) + "입니다.”  // 소수점 넷째자리로 끊음
        });
        iwContent.open(map, marker); // 마커 위에 인포 윈도우 표시

        iwContents.push(iwContent); // 위에 선언한 객체 배열에 새로 생성된 인포윈도우를 저장
      }

그런데 위의 코드로 실행하게 되면 마커가 드래그 되어 다른 위치에 갈 경우 인포윈도우는 원래의 위치에 그대로 남아있음을 볼 수 있습니다. 이걸 해결하기 위해, 드래그 되는 이벤트도 사용해 보도록 합시다.
 
다음 api 라이브러리를 보면 드래그에 dragstart와 dragend 두개의 이벤트가 있습니다. 우리의 경우는 드래그가 완료되었을 때 인포윈도우가 위치를 바뀌기를 원하는 것이므로 dragend를 이용해야합니다.
 
daum.maps.event.addListener(marker, 'dragend', function() {
          // 출발 마커의 드래그가 종료될 때 인포윈도우의 내용을 바꿔주면서 기존의 인포윈도우는 닫고 새 위치에 창을 열어줍니다

          iwContent.close(map, marker); // 기존의 인포윈도우 닫기

          iwContent =  new daum.maps.InfoWindow({ // 인포 윈도우 내용 바꾸기기
           position : marker.getPosition(),
            content : "마커의 위치는 " + marker.getPosition().getLat().toFixed(4) + "," + marker.getPosition().getLng().toFixed(4) + "입니다.",
            removable: true
          });

          iwContent.open(map, marker); //새 내용으로 오픈
          iwContents.pop(); // 기존의 내용 제거
          iwContents.push(iwContent); // 새 내용 삽입
        });

addMarker 함수 내부에 위의 내용을 더 추가 해주면 마커가 이동할 때 인포윈도우도 함께 움직이게 됩니다!

그럼 이제 사용자의 위치를 자동으로 잡아오는 geolocation도 사용해 봅시다.
샘플 페이지의 geolocation으로 마커 표시하기를 응용하여 처음에 접속하면 사용자의 위치가 바로 찍히고 추가로 지도를 클릭하면 마커가 생성되게 하는 것 입니다.

 

if (navigator.geolocation) {
       // GeoLocation을 이용해서 접속 위치를 얻어옵니다
       navigator.geolocation.getCurrentPosition(function(position) {

         var lat = position.coords.latitude; // 위도
         var lon = position.coords.longitude; // 경도

         var locPosition = new daum.maps.LatLng(lat, lon); // 마커가 표시될 위치를 geolocation으로 얻어온 좌표로 생성합니다
         var message = '<div style="padding:5px;">현재 위치</div>'; // 인포윈도우에 표시될 내용입니다

         // 마커와 인포윈도우를 표시합니다
         displayMarker(locPosition, message);
       });

     } else { // HTML5의 GeoLocation을 사용할 수 없을때 마커 표시 위치와 인포윈도우 내용을 설정합니다
       var locPosition = new daum.maps.LatLng(33.450701, 126.570667);
       var message = 'geolocation을 사용할수 없어요..’;

       displayMarker(locPosition, message);
     }

     // 지도에 마커와 인포윈도우를 표시하는 함수입니다
     function displayMarker(locPosition, message) {
       // 마커를 생성합니다
       var geoMarker = new daum.maps.Marker({
         map: map,
         position: locPosition
       });

       var geoiwContent = message// 인포윈도우에 표시할 내용

       // 인포윈도우를 생성합니다
       var infowindow = new daum.maps.InfoWindow({
         content : geoiwContent
       });

       // 인포윈도우를 마커위에 표시합니다
       infowindow.open(map, geoMarker);

       // 지도 중심좌표를 접속위치로 변경합니다
       map.setCenter(locPosition);
     }

 

여기까지 기본으로 필요한 내용을 살펴 보았습니다. 이 내용들을 바탕으로 응용하여 더 진행해볼까요?
다음엔 중간 지점 찾는 기능을 구현해보도록 하겠습니다!


 

2. 두 지점의 중점 찾기

 

(출처:http://www.geomidpoint.com/meet/)

위의 지도와 같이 두 지점을 선택하면 그 가운데 위치를 찍어 그 주변 먹거리, 놀 거리를 추천해주는 것입니다.
이때 가운데 지점은 기본적인 수학에서 배웠던 좌표를 통해 구할 수 있습니다. 두 지점의 경도와 위도를 잡아와서 그 평균값을 가지는 새 경도와 위도를 찍어주면 됩니다.
즉, (x1, y1), (x2, y2)의 좌표의 중점은 ((x1+x2)/2,(y1+y2)/2)의 값을 가지게 됩니다.
그래서 그 중점을 찾고 반경 500m (혹은 1km)이내에서 장소를 추천하도록 합시다.

그럼 먼저 두 지점의 중점을 지도에 표시하는 것부터 구현해봅시다.
먼저 마커의 위치 정보만을 가지고 있을 배열을 선언 해줍니다.
var markersPos = []; // marker의 위치만을 가지고 있을 배열
이 배열에 우리가 앞으로 사용할 마커들의 위치(위도, 경도)가 저장됩니다.
그리고 두개의 마커만을 이용할 것이므로, 지도에 두개 이상 마커가 찍힐 경우 알림창을 띄워주게 합니다. 이때 비교는, 위에서 선언한 markersPos의 길이로 구분합니다.
지도에 마커가 생기면 그 위치 정보를 저 배열에 넣으므로 배열의 길이로 구분 되는 것입니다.지도를 클릭했을 때 실행되는 함수 내부에
 var length = markersPos.length;

if(length > 1) {
   alert("두개만 찍을 수 이뜸");
} else {
  // 마커를 생성합니다
   addMarker(mouseEvent.latLng);
}
로 만들어 줍니다.
그리고 addMarker에는
var markerPos = { // 마커의 위치 정보
            newLat: position.getLat().toFixed(4),
            newLng: position.getLng().toFixed(4)
          };
의 형태를 가지는 변수를 선언해줍니다.
그리고 markersPos.push(markerPos); 를 통해 배열 내에 새로 추가 해줍니다.
여기까지가 두개의 위치를 찍는 부분이었습니다.

 


 
그럼 지도 아래 submit 버튼을 만들어 버튼을 누르면 두 지점의 중점을 찾는 함수를 만들어 볼까요?
먼저 html 부분에 버튼을 하나 만들어줍니다.

지도 밑에 아래 코드를 넣어줍니다. 클릭을 하면 findMidPosition() 이라는 함수를 호출하게 됩니다.

<button href="" onclick="findMidPosition();">submit</button>
findMidPosition이라는 함수를 만들어주고, 지도에 마커가 표시되어있는지 확인하기 위한 if문을 만들어 줍니다.
위에서 마커 갯수 확인해주는것과 마찬가지로
if(markersPos.length != 2) {
          alert("지도에 마커를 두개 찍어주세용");
        } else {
// 중점 계산
}

을 통해 구현해줍니다.

그리고 중점은 위에서 이야기한 계산법을 통해 구해줍니다. 이때를 위해 아까 만들어 두었던 markersPos 배열을 이용합니다.

var marker1Lat = markersPos[0].newLat * 1;
var marker1Lng = markersPos[0].newLng * 1;

이렇게 marker1,2의 경도 위도 값을 가져옵니다. 이때 1을 곱해주는건 정수형으로 바꿔주기 위함입니다.

그리고 중점은,

var newPosLat = ((marker1Lat + marker2Lat) / 2).toFixed(4);
var newPosLng = ((marker1Lng + marker2Lng) / 2).toFixed(4);
이렇게 구하여 소수점 아래 자리수까지 맞춰줍니다.
이제 지도상에 표시를 해볼까용:)
마커를 표시하는건 똑같이 해주면 됩니다. 이때 한가지 미리 선언해줘야 할것은, 중점만을 저장하는 midPoint배열입니다! 함수 밖에
var midPoint = []; 로 선언 해주고, 중점 마커를 생성하여 저장해줍니다.
// 두 마커간의 중점 찍는 거
var marker = new daum.maps.Marker({
    map: map
});

midPoint.push(marker);
marker.setPosition(new daum.maps.LatLng(newPosLat, newPosLng));
marker.setMap(map);
여기까지를 정리하면 다음과 같은 코드가 됩니다.
var midPoint = [];

      function findMidPosition () {
        // 버튼 누르면 두 지점의 중점을 찾음

        if(markersPos.length != 2) {
          alert("지도에 마커를 두개 찍어주세용");
        } else {
          // 중점 계산하기
          var marker1Lat = markersPos[0].newLat * 1;
          var marker1Lng = markersPos[0].newLng * 1;
          var marker2Lat = markersPos[1].newLat * 1;
          var marker2Lng = markersPos[1].newLng * 1;

          var newPosLat = ((marker1Lat + marker2Lat) / 2).toFixed(4);
          var newPosLng = ((marker1Lng + marker2Lng) / 2).toFixed(4);

          $('.position_mid').text(newPosLat + "," + newPosLng);

          // 두 마커간의 중점 찍는 거
          var marker = new daum.maps.Marker({
            map: map
          });

          midPoint.push(marker);
          marker.setPosition(new daum.maps.LatLng(newPosLat, newPosLng));
          marker.setMap(map);
        }
      }


  // 지도에 마커와 인포윈도우를 표시하는 함수입니다
      function displayMarker(locPosition, message) {

        // 마커를 생성합니다
        var geoMarker = new daum.maps.Marker({
          map: map,
          position: locPosition
        });

        var geoiwContent = message// 인포윈도우에 표시할 내용

        // 인포윈도우를 생성합니다
        var infowindow = new daum.maps.InfoWindow({
          content : geoiwContent
        });

        // 인포윈도우를 마커위에 표시합니다
        infowindow.open(map, geoMarker);

        // 지도 중심좌표를 접속위치로 변경합니다
        map.setCenter(locPosition);
      }

      daum.maps.event.addListener(map, 'click', function(mouseEvent) {
        // 클릭한 위치에 마커를 추가하는 함수

        var length = markersPos.length;
        if(length > 1) {
          alert("두개만 찍을 수 이뜸");
        } else {
          // 마커를 생성합니다
          addMarker(mouseEvent.latLng);
        }
      });

 // 마커를 생성하고 지도위에 표시하는 함수입니다
      function addMarker(position) {
        var marker = new daum.maps.Marker({
          map: map,
          position: position,
          draggable: true // 드래그 가능하게
        });

        var markerPos = { // 마커의 위치 정보
          newLat: position.getLat().toFixed(4),
          newLng: position.getLng().toFixed(4)
        };

        var iwContent = new daum.maps.InfoWindow({
          position: position,
          content: "마커의 위치는 " + position.getLat().toFixed(4) + "," + position.getLng().toFixed(4) + "입니다.",
          removable: true
        });

        // 마커가 지도 위에 표시되도록 설정합니다
        marker.setMap(map);
        iwContent.open(map, marker);

        // 생성된 마커를 배열에 추가합니다
        markers.push(marker);
        iwContents.push(iwContent);
        markersPos.push(markerPos);

        daum.maps.event.addListener(marker, 'dragend', function () {
          // 출발 마커의 드래그가 종료될 때 인포윈도우의 내용을 바꿔주면서 기존의 인포윈도우는 닫고 새 위치에 창을 열어줍니다

          iwContent.close(map, marker); // 기존의 인포윈도우 닫기

          iwContent = new daum.maps.InfoWindow({ // 인포 윈도우 내용 바꾸기기
            position: marker.getPosition(),
            content: "마커의 위치는 " + marker.getPosition().getLat().toFixed(4) + "," + marker.getPosition().getLng().toFixed(4) + "입니다.",
            removable: true
          });
          iwContent.open(map, marker); //새 내용으로 오픈
        });
      }

 

이제 여기서 또다른 경우를 생각해줍니다! 만약 마커 1,2를 드래그해서 다른 지점으로 옮길 경우는 어떻게 해야하는가!
이때는 drag 이벤트를 이용해줍니다. 먼저 드래그되어지는 마커가 무엇인지 알기 위해, dragstart 이벤트에선 마커의 기존 위치를 찾아줍니다.
daum.maps.event.addListener(marker, 'dragstart', function () {
   draggedMarker.Lat = marker.getPosition().getLat().toFixed(4);
   draggedMarker.Lng = marker.getPosition().getLng().toFixed(4);

   for(var i=0; i<markersPos.length; i++) { // 드래그 되는 마커를 알기 위해
      if(draggedMarker.Lat == markersPos[i].newLat && draggedMarker.Lng == markersPos[i].newLng){
          markerNum = i;
      }
   }
});
markersPos에 저장된 배열 중 몇번째인지 확인해서 그 i값을 가져옵니다.
그리고 드래그가 끝났을 때는 i의 값을 이용해 배열의 값을 바꿔줍니다.
daum.maps.event.addListener(marker, 'dragend', function () {
   // 출발 마커의 드래그가 종료될 때 인포윈도우의 내용을 바꿔주면서 기존의 인포윈도우는 닫고 새 위치에 창을 열어줍니다
   iwContent.close(map, marker); // 기존의 인포윈도우 닫기 

   iwContent = new daum.maps.InfoWindow({ // 인포 윈도우 내용 바꾸기기
      position: marker.getPosition(),
      content: "마커의 위치는 " + marker.getPosition().getLat().toFixed(4) + "," + marker.getPosition().getLng().toFixed(4) + "입니다.",
      removable: true
   }); 

   var newPos = { //드래그 된 마커의 새 위치 정보
       newLat: marker.getPosition().getLat().toFixed(4),
       newLng: marker.getPosition().getLng().toFixed(4)
   };
   markersPos[markerNum] = newPos;
   iwContent.open(map, marker); //새 내용으로 오픈
   iwContents[markerNum] = iwContent; // 새 내용 삽입
});
이 상태에서 submit 버튼을 다시 누르게 되면 중점이 두개가 찍히게 됩니다!
이것은 다음과 같이 정리해줍니다.
위에서 선언했던 midPoint 배열을 이용해서
if (midPoint.length == 1) { // 기존에 이미 존재하면
    midPoint[0].setMap(null); // 지도에서 없애고
    midPoint.pop(); // 배열에서 빼줌
}
여기까지가 두 지점의 중점 찍기 였습니다!
총 정리를 하게 되면 다음과 같아요!
// 마커를 생성하고 지도위에 표시하는 함수입니다
      function addMarker(position) {
        var marker = new daum.maps.Marker({
          map: map,
          position: position,
          draggable: true // 드래그 가능하게
        });

        var markerPos = { // 마커의 위치 정보
          newLat: position.getLat().toFixed(4),
          newLng: position.getLng().toFixed(4)
        };

        var iwContent = new daum.maps.InfoWindow({
          position: position,
          content: "마커의 위치는 " + position.getLat().toFixed(4) + "," + position.getLng().toFixed(4) + "입니다.",
          removable: true
        });

        // 마커가 지도 위에 표시되도록 설정합니다
        marker.setMap(map);
        iwContent.open(map, marker);

        // 생성된 마커를 배열에 추가합니다
        markers.push(marker);
        iwContents.push(iwContent);
        markersPos.push(markerPos);

        var draggedMarker = {};
        var markerNum = 0;

       
        // ----------- 드래그 추가 된 부분 ------------//
        daum.maps.event.addListener(marker, 'dragstart', function () {
          draggedMarker.Lat = marker.getPosition().getLat().toFixed(4);
          draggedMarker.Lng = marker.getPosition().getLng().toFixed(4);

          for(var i=0; i<markersPos.length; i++) { // 드래그 되는 마커를 알기 위해
            if(draggedMarker.Lat == markersPos[i].newLat && draggedMarker.Lng == markersPos[i].newLng){
              markerNum = i;
            }
          }
        });

        daum.maps.event.addListener(marker, 'dragend', function () {
          // 출발 마커의 드래그가 종료될 때 인포윈도우의 내용을 바꿔주면서 기존의 인포윈도우는 닫고 새 위치에 창을 열어줍니다

          iwContent.close(map, marker); // 기존의 인포윈도우 닫기

          iwContent = new daum.maps.InfoWindow({ // 인포 윈도우 내용 바꾸기기
            position: marker.getPosition(),
            content: "마커의 위치는 " + marker.getPosition().getLat().toFixed(4) + "," + marker.getPosition().getLng().toFixed(4) + "입니다.",
            removable: true
          });

          var newPos = { //드래그 된 마커의 새 위치 정보
            newLat: marker.getPosition().getLat().toFixed(4),
            newLng: marker.getPosition().getLng().toFixed(4)
          };

          markersPos[markerNum] = newPos;

          iwContent.open(map, marker); //새 내용으로 오픈
          iwContents[markerNum] = iwContent; // 새 내용 삽입
        });
      }


function findMidPosition () {
        // 버튼 누르면 두 지점의 중점을 찾음

        if(markersPos.length != 2) {
          alert("지도에 마커를 두개 찍어주세용");
        } else {
          // 중점 계산하기
          var marker1Lat = markersPos[0].newLat * 1;
          var marker1Lng = markersPos[0].newLng * 1;
          var marker2Lat = markersPos[1].newLat * 1;
          var marker2Lng = markersPos[1].newLng * 1;

          var newPosLat = ((marker1Lat + marker2Lat) / 2).toFixed(4);
          var newPosLng = ((marker1Lng + marker2Lng) / 2).toFixed(4);

          $('.position_mid').text(newPosLat + "," + newPosLng);

          // 두 마커간의 중점 찍는 거
          var marker = new daum.maps.Marker({
            map: map
          });
    

          // ----------- 추가 된 부분 ------------//
          if (midPoint.length == 1) {
            midPoint[0].setMap(null);
            midPoint.pop();
          }

          midPoint.push(marker);
          marker.setPosition(new daum.maps.LatLng(newPosLat, newPosLng));
          marker.setMap(map);
        }
      }

여기까지가 두 지점의 중점을 찾고 마커를 찍어주는 것이었습니다. 그러면 다음엔 각 지점에서 중점까지 가는 경로를 탐색하고 그 결과를 지도에 표시하는 것을 구현해보도록 하겠습니다:)

 


 

3. 중점 주변 카테고리 검색

 
프로젝트를 진행하다보니 다음 api는 경로 탐색은 제공하지 않는다는 사실을 알았습니다.
그래서 다음과 같이 프로세스를 바꾸기로 했습니다!

두 명의 위치를 찍고 원하는 카테고리를 선택하면 중점을 찍고 그 점을 중심으로 반경 1000미터 내의 카테고리 결과물을 마커로 찍어주는 것입니다. 여기서 좀더 업그레이드하면 반경의 거리도 바꿀 수 있겠죠?
아무튼 결과물은 다음과 같은 식으로 나오게 됩니다.

그리고 각 마커를 클릭하면 다음처럼 커스텀 창으로 정보를 안내해줍니다.

그리고 오버레이를 클릭하면 다음에서 검색되는 것으로 연결됩니다.

여기까지 구현해보겠습니다!

먼저 커스텀 오버레이를 이용한 인포윈도우를 만들어볼까요? 기본 api로 제공하는 info window는 크기도 조절되지 않고 이쁘지가 않습니다! 커스텀 오버레이를 이용하면 css로 조절이 가능해 이쁘게 만들 수 있습니다!

 

sample 페이지에 나온 코드들을 응용해서 만들어 보겠습니다.

// 함수 밖에 전역 변수로 선언해줌
    var overlay_mid = {}; // 중점 표시 오버레이
    var overlay_user = {}; // 유저 표시 오버레이

// addMarker 함수에 infowindow 부분을 지웁니다!
// 대신 overlay를 추가      
function addMarker(position) {
......
var content = '<div class="wrap">' +
              '    <div class="info">' +
              '        <div class="title">' +
              '            USER ' + userIndex +
              '        </div>' +
              '    </div>' +
              '</div>';

      // 마커 위에 커스텀오버레이를 표시합니다
      // 마커를 중심으로 커스텀 오버레이를 표시하기위해 CSS를 이용해 위치를 설정했습니다
      overlay_user = new daum.maps.CustomOverlay({
        content: content,
        map: map,
        position: marker.getPosition()
      });

      // 마커가 지도 위에 표시되도록 설정합니다
      marker.setMap(map);
      overlay_user.setMap(map); // 오버레이 지도에 표시
......
}

여기까지는 user의 마커 두개를 표시한 것이고 중점 마커는 아래와 같습니다.

function  findMidPosition() {
......
// 중점마커의 인포윈도우는 커스텀 오버레이로 나타내줌
        var content = '<div class="wrap middle">' +
                '    <div class="info">' +
                '        <div class="title">' +
                '            중점' +
                '        </div>' +
                '    </div>' +
                '</div>';

        overlay_mid = new daum.maps.CustomOverlay({
          content: content,
          map: map,
          position: marker_mid.getPosition(),
          zIndex: 10
        });

        overlay_mid.setMap(map);
....
}

 

그리고 css부분은 다음과 같습니다.

.wrap {position: absolute;left: 0;bottom: 35px;width: 60px;height: 28px;border: 1px solid #777; background-color: #777; color: white; opacity: 0.7;margin-left: -30px;text-align: center;overflow: hidden;font-size: 12px;font-family: 'Malgun Gothic', dotum, '돋움', sans-serif;line-height: 1.5;}
.wrap * {padding: 0;margin: 0;}
.middle {border: 1px solid #777; background-color: #777; opacity: 0.4;color: red;}
.info .title {padding: 5px 0 0 0;height: 28px;font-size: 15px;}

(그 외 css는 다음의 샘플 코드를 참조합니다.)

이 코드를 실행해보면,

이렇게 나오게 됩니다. 여기서 원은 두 마커간의 중심 지점을 중점으로 하는 반경 1000미터의 원입니다.

다음 단계로 이 반경 내부의 카테고리를 통한 주변 시설 검색을 할 것이므로 원을 표시하는 방법을 알아보겠습니다.

원은 간단합니다.

// 중점 마커를 중심으로 하는 원을 그려줌
       var circles = new daum.maps.Circle({
          center : new daum.maps.LatLng(newPosLat, newPosLng),  // 원의 중심좌표 입니다
          radius: 1000, // 미터 단위의 원의 반지름입니다
          strokeWeight: 5, // 선의 두께입니다
          strokeColor: '#75B8FA', // 선의 색깔입니다
          strokeOpacity: 1, // 선의 불투명도 입니다 1에서 0 사이의 값이며 0에 가까울수록 투명합니다
          strokeStyle: 'dashed', // 선의 스타일 입니다
          fillColor: '#CFE7FF', // 채우기 색깔입니다
          fillOpacity: 0.7  // 채우기 불투명도 입니다
        });

        // 지도에 원을 표시합니다
        circles.setMap(map);

여기서 중심좌표는 두 마커간의 중간 지점이 되게 됩니다.

여기까지면 다 준비 되었고 카테고리로 검색한 결과를 표시하기로 본격 들어가봅시다!

 

다음 api sample과 docs를 응용하면 만들지 못할 것이 없습니다! 샘플 목록에 보면 카테고리로 검색하기가 있는데 우리는 이것을 이용할 것입니다.
그럼 시작하기에 앞서 주의해야할 부분들을 집고 넘어가봅시다.
우선 프로세스는 다음과 같아요.
두명의 유저 마커 표시 ->  submit -> 중점 찾기(기존에 카테고리 마커가 있을 경우 삭제) -> 원그리기 -> (기존에 중점과 원이 있을 경우 삭제) -> 카테고리 나타남 -> 선택한 카테고리 마커 찍어줌
여기서 초기화가 매우 중요합니다!! 잊지 말아주세요!
 
그럼 이제 코드를 살펴봅시다! 자세한 내용은 주석으로 달아놨으니 주석을 꼼꼼하게 봐 주세요!
(다음  샘플 페이지를 응용했습니다. http://apis.map.daum.net/web/sample/categoryFromBounds/)
(윗부분에서 살짝 수정된 부분이 있어서 코드를 대부분 다시 추가하겠습니다.)
 
<!-- html 코드 변경 내용 -->
<!-- 카테고리 -->
<div class="listPart">
  <div class="selection">
    <div class="selection_btn">
      두 지점을 선택한 후 버튼을 눌러주세요
      <br>
      <button href="" onclick="findMidPosition();">submit</button>
    </div>
    <div class="selection_category">
      <ul id="category">
        <li id="BK9" data-order="0">
          <span class="category_bg bank"></span>
          은행
        </li>
        <li id="MT1" data-order="1">
          <span class="category_bg mart"></span>
          마트
        </li>
        <li id="CE7" data-order="4">
          <span class="category_bg cafe"></span>
          카페
        </li>
      </ul>
    </div>
  </div>
</div>

스크립트 전역 변수 선언

// 마커를 클릭했을 때 해당 장소의 상세정보를 보여줄 커스텀오버레이
var placeOverlay = new daum.maps.CustomOverlay({zIndex:100});
var contentNode = document.createElement('div'); // 커스텀 오버레이의 컨텐츠 엘리먼트 입니다

var currCategory = ''; // 현재 선택된 카테고리를 가지고 있을 변수입니다

var resultMarkers = []; // 결과 마커를 담을 배열입니다
var userMarkers = []; // 유저 마커를 담을 배열
var usersPos = []; // 유저 마커의 위치만을 담을 배열
var overlay_user_array = []; // 유저 마커의 오버레이를 저장할 배열
var midMarkers = []; // 중점 마커를 담을 배열
var newPosLat = 0; // 중점 마커 위치정보
var newPosLng = 0; // 중점 마커 위치정보

var circles = {}; // 검색될 범위에 원을 그려줌
var overlay_mid = {}; // 중점 표시 오버레이
var overlay_user = {}; // 유저 표시 오버레이

var container = document.getElementById('map'); //지도를 담을 영역의 DOM 레퍼런스
var options = { //지도를 생성할 때 필요한 기본 옵션
  center: new daum.maps.LatLng(33.450701, 126.570667), //지도의 중심좌표.
  level: 3 //지도의 레벨(확대, 축소 정도)
};

var map = new daum.maps.Map(container, options); //지도 생성 및 객체 리턴

// 장소 검색 객체를 생성합니다
var ps = new daum.maps.services.Places(map);
// 커스텀 오버레이의 컨텐츠 노드에 css class를 추가합니다
contentNode.className = 'placeinfo_wrap';

// 커스텀 오버레이 컨텐츠를 설정합니다
placeOverlay.setContent(contentNode);

중점을 찾는 함수입니다.

findMidPosition ();

두 마커를 찍고 버튼을 누르면 맵 위의 결과 마커가 있을 경우 초기화 시키고 중점을 새로 찍은 후 검색 영역을 나타내는 원을 그려준다.
그리고 hide() 해두었던 카테고리 영역을 show() 해줍니다.

function findMidPosition() {

  removeMarker(); // 먼저 기존에 있는 마커들 삭제

  var imageSrc = '/images/location-pin_red.png'; // 마커이미지의 주소입니다
  var imageSize = new daum.maps.Size(32, 32); // 마커이미지의 크기입니다
  var imageOption = {offset: new daum.maps.Point(15, 32)}; // 마커이미지의 옵션입니다. 마커의 좌표와 일치시킬 이미지 안에서의 좌표를 설정합니다.

  // 마커의 이미지정보를 가지고 있는 마커이미지를 생성합다
  var markerImage = new daum.maps.MarkerImage(imageSrc, imageSize, imageOption);

  if(usersPos.length != 2) { // 마커를 두개 찍어야함
    alert("지도에 마커를 두개 찍어주세용");
  } else {
    // 중점 계산하기
    var marker1Lat = usersPos[0].newLat * 1;
    var marker1Lng = usersPos[0].newLng * 1;
    var marker2Lat = usersPos[1].newLat * 1;
    var marker2Lng = usersPos[1].newLng * 1;

    newPosLat = ((marker1Lat + marker2Lat) / 2).toFixed(4);
    newPosLng = ((marker1Lng + marker2Lng) / 2).toFixed(4);

    // 중점 마커를 찍는 거
    var marker_mid = new daum.maps.Marker({
      map: map,
      image: markerImage
    });

    if (midMarkers.length == 1) { // 이미 중점이 있을 경우 없애주고 다시 찍어야하므로
      midMarkers[0].setMap(null);
      midMarkers.pop();

      // 기존의 원과 중점 오버레이 없애줌
      circles.setMap(null);
      overlay_mid.setMap(null);
    }

    // 중점 마커 배열에 다시 새로 넣어줌
    midMarkers.push(marker_mid);
    marker_mid.setPosition(new daum.maps.LatLng(newPosLat, newPosLng));
    marker_mid.setMap(map);

    // 중점마커의 인포윈도우는 커스텀 오버레이로 나타내줌
    var content = '<div class="wrap middle">' +
            '    <div class="info">' +
            '        <div class="title">' +
            '            중점' +
            '        </div>' +
            '    </div>' +
            '</div>';

    overlay_mid = new daum.maps.CustomOverlay({
      content: content,
      map: map,
      position: marker_mid.getPosition(),
      zIndex: 10
    });

    overlay_mid.setMap(map);

    // 중점 마커를 중심으로 하는 원을 그려줌
    circles = new daum.maps.Circle({
      center : new daum.maps.LatLng(newPosLat, newPosLng),  // 원의 중심좌표 입니다
      radius: 1000, // 미터 단위의 원의 반지름입니다
      strokeWeight: 5, // 선의 두께입니다
      strokeColor: '#75B8FA', // 선의 색깔입니다
      strokeOpacity: 1, // 선의 불투명도 입니다 1에서 0 사이의 값이며 0에 가까울수록 투명합니다
      strokeStyle: 'dashed', // 선의 스타일 입니다
      fillColor: '#CFE7FF', // 채우기 색깔입니다
      fillOpacity: 0.7  // 채우기 불투명도 입니다
    });

    // 지도에 원을 표시합니다
    circles.setMap(map);
    map.setCenter(new daum.maps.LatLng(newPosLat, newPosLng));
    map.setLevel(5);
    $('#category').show(); // 중점을 찾고 선택할 수 있는 카테고리를 보여줍니다
  }
}

 

카테고리에 클릭 이벤트를 등록합니다.

 

addCategoryClickEvent();

// 각 카테고리에 클릭 이벤트를 등록합니다
function addCategoryClickEvent() {
  var category = document.getElementById('category'),
          children = category.children;

  for (var i=0; i<children.length; i++) {
    children[i].onclick = onClickCategory;
  }
}

function onClickCategory() {
  var id = this.id,
          className = this.className;

  placeOverlay.setMap(null);

  if (className === 'on') {
    currCategory = '';
    changeCategoryClass();
    removeMarker();
  } else {
    currCategory = id;
    changeCategoryClass(this);
    searchPlaces();
  }
}

// 클릭된 카테고리에만 클릭된 스타일을 적용하는 함수입니다
function changeCategoryClass(el) {
  var category = document.getElementById('category'),
          children = category.children,
          i;

  for ( i=0; i<children.length; i++ ) {
    children[i].className = '';
  }

  if (el) {
    el.className = 'on';
  }
}

이제 카테고리 선택까지 되었으면 선택된 카테고리 값을 받아와 검색하는 부분으로 넘어가겠습니다.

// 카테고리 검색을 요청하는 함수입니다
    function searchPlaces() {
      if (!currCategory) {
        return;
      }

      // 커스텀 오버레이를 숨깁니다
      placeOverlay.setMap(null);

      // 지도에 표시되고 있는 마커를 제거합니다
      removeMarker();

      ps.categorySearch(currCategory, placesSearchCB, {location:new daum.maps.LatLng(newPosLat, newPosLng), radius: 1000});
    }

    // 장소검색이 완료됐을 때 호출되는 콜백함수 입니다
    function placesSearchCB( status, data, pagination ) {
      if (status === daum.maps.services.Status.OK) {

        // 정상적으로 검색이 완료됐으면 지도에 마커를 표출합니다
        displayPlaces(data.places);
      } else if (status === daum.maps.services.Status.ZERO_RESULT) {
        // 검색결과가 없는경우 
         alert("검색 결과가 없습니다!");
      } else if (status === daum.maps.services.Status.ERROR) {
        // 에러로 인해 검색결과가 나오지 않은 경우 
         alert("ERROR!!");
      }
    }

    // 지도에 마커를 표출하는 함수입니다
    function displayPlaces(places) {

      // 몇번째 카테고리가 선택되어 있는지 얻어옵니다
      // 이 순서는 스프라이트 이미지에서의 위치를 계산하는데 사용됩니다
      var order = document.getElementById(currCategory).getAttribute('data-order');
      for ( var i=0; i<places.length; i++ ) {

        // 마커를 생성하고 지도에 표시합니다
        var marker = addMarkerCate(new daum.maps.LatLng(places[i].latitude, places[i].longitude), order);

        // 마커와 검색결과 항목을 클릭 했을 때
        // 장소정보를 표출하도록 클릭 이벤트를 등록합니다
        (function(marker, place) {
          daum.maps.event.addListener(marker, 'click', function() {
            displayPlaceInfo(place);
          });
        })(marker, places[i]);
      }
    }

    // 마커를 생성하고 지도 위에 마커를 표시하는 함수입니다
    function addMarkerCate(position, order) {
      var imageSrc = 'http://i1.daumcdn.net/localimg/localimages/07/mapapidoc/places_category.png', // 마커 이미지 url, 스프라이트 이미지를 씁니다
              imageSize = new daum.maps.Size(27, 28),  // 마커 이미지의 크기
              imgOptions =  {
                spriteSize : new daum.maps.Size(72, 208), // 스프라이트 이미지의 크기
                spriteOrigin : new daum.maps.Point(46, (order*36)), // 스프라이트 이미지 중 사용할 영역의 좌상단 좌표
                offset: new daum.maps.Point(11, 28) // 마커 좌표에 일치시킬 이미지 내에서의 좌표
              },
              markerImage = new daum.maps.MarkerImage(imageSrc, imageSize, imgOptions),
              marker = new daum.maps.Marker({
                position: position, // 마커의 위치
                image: markerImage
              });

      marker.setMap(map); // 지도 위에 마커를 표출합니다
      resultMarkers.push(marker);  // 배열에 생성된 마커를 추가합니다

      return marker;
    }

    // 지도 위에 표시되고 있는 마커를 모두 제거합니다
    function removeMarker() {
      for ( var i = 0; i < resultMarkers.length; i++ ) {
        resultMarkers[i].setMap(null);
      }
      resultMarkers = [];
    }

    // 클릭한 마커에 대한 장소 상세정보를 커스텀 오버레이로 표시하는 함수입니다
    function displayPlaceInfo (place) {
      var content = '<div class="placeinfo">' +
              '   <a class="title" href="' + place.placeUrl + '" target="_blank" title="' + place.title + '">' + place.title + '</a>';

      if (place.newAddress) {
        content += '    <span title="' + place.newAddress + '">' + place.newAddress + '</span>' +
                '  <span class="jibun" title="' + place.address + '">(지번 : ' + place.address + ')</span>';
      }  else {
        content += '    <span title="' + place.address + '">' + place.address + '</span>';
      }

      content += '    <span class="tel">' + place.phone + '</span>' +
              '</div>' +
              '<div class="after"></div>';

      contentNode.innerHTML = content;
      placeOverlay.setPosition(new daum.maps.LatLng(place.latitude, place.longitude));
      placeOverlay.setMap(map);
    }

여기까지 하면 기본적으로 두 지점의 중점과 그 주변의 카테고리 별 시설을 검색하는 기능을 구현했습니다. 더 다양한 카테고리는 다음의 api 페이지가면 많이 볼 수 있어요!

 

RYUHA

만들어 보면서 하나씩 글 씁니당:)

14
Leave a Reply

avatar
6 Comment threads
8 Thread replies
1 Followers
 
Most reacted comment
Hottest comment thread
6 Comment authors
문지현RYUHAyou윤현성jongdae Recent comment authors
newest oldest most voted
Yooooon
Guest
Yooooon

중간지점이라 하면 (0, 0)/(4, 6)의 경우 (2,3) 이렇게 나오는 건가요? 그 지점이 산일 경우는 어떻게 하나요,,,ㅎ

Yooooon
Guest
Yooooon

뒤에 글을 보니 중점을 잡고 주변 카테고리를 검색 하던데 혹시 교통수단을 이용한 중간지점 찾기(?)는 어떻게 구현해볼 수 있을까요 ㅠ

RYUHA
Guest
RYUHA

넹 중간지점 그렇게 나오는 것 맞습니당! 그리고 그 지점이 산인 경우에도 처음에 설정된 반경에 의해 주변 카테고리 검색을 하게 되고 그 결과 가 없으면 없음으로 알림이 뜹니다. 그럴 경우를 대비해 반경 역시 사용자가 선택할 수 있는 select box를 추가하려고 합니다. 또 api 특성상 교통수단으로 경로 탐색을 하는 것을 맵에다가 바로 구현은 할 수 가 없고 다음 지도로 링크를 타고 가게 되더라구요 그래서 차선택으로 카테고리 검색 결과를 마커로 찍어주는 거까지만 구현했습니다:(

jongdae
Guest
jongdae

아.. 안그래도 저도 궁금해서 따로 댓글로 질문했었는데…
그렇군요. 경로탐색은 바로 구현할 수 없군요.

차계부 앱등에 쓸 수 있을까 하여 궁금한데요.
두 지점간의 일반적인 도로의 구간 거리값만을 구하는 것도
직접 구현은 막혀있을까요?

의견 부탁드립니다^^;;

jongdae
Guest
jongdae

와우!! 정말 깜짝 놀랐습니다.
아이디어도 좋구요.

그런데 서두에는 그 중간지점을 찾아가는 경로를 탐색하는 것까지 있는데.
경로 찾기는 어떻게 하는지 … 궁금해요!!

따뜻해지는 지금이 감기 걸리기 쉽다고 합니다.
건강유의하세요.

윤현성
Member

그러면 다음 글에선 마무리를 하고 최종 완성본을 함께 보도록 해보아요! <== 이 부분은 어디 있나요?

you
Guest
you

다른 기능은 다 되는데 ps 변수에 장소검색기능 가져오는게 왜 않될까요? ㅎ 혹시 예상되는문제점 있으시면 답변 부탁 드립니다.

you
Guest
you

ps 변수에 장소 검색 가져오는 부분만 않되네요. 혹시 의심 가시는 점 있으면 좀 알려주세요.

문지현
Guest
문지현

혹시 다음 API는 자동완성 기능이 제공되지 않는가요?! 제공된다면 사용방법좀 알려주실 수 있을까요ㅠㅠ 참고로 안드로이드 제작중입니다..!