OpenAPI/C# OpenAPI

5. Final 경로 찾기 Windows Forms 앱[경로찾기 프로젝트 – C#, 카카오 로컬, 지도 API, SK TMAP API 활용 ]

언휴 2024. 1. 9. 08:38

안녕하세요. 언제나휴일입니다.

1. 유튜브 동영상 강의

마지막 프로젝트로 경로 찾기 Windows Forms 앱입니다.

2. 프로그램 소개

이번에 만들 프로젝트는 C#, Windows Forms(.NET Framework) 으로 만들 것입니다.

어떠한 프로그램을 만들 것인지 동작 화면을 보기로 할게요.

경로 찾기 프로그램 동작 화면
경로 찾기 프로그램 동작 화면

출발지와 목적지를 입력하면 검색 결과를 리스트 박스에 보여줍니다.

출발지와 목적지를 리스트 박스에서 선택한 후 경로 찾기 버튼을 누르면 지도에 경로를 선으로 표시합니다.

그리고 리스트 박스에 경로를 요소로 보여줍니다.

시뮬레이션 버튼을 클릭하면 출발지에서 목적지까지 이동하는 모습을 볼수 있습니다.

3. 지도 HTML 코드 편집

앞에서 작성했던 KakaoMap.html 코드를 추가 편집하기로 할게요.

먼저 지도의 너비와 높이를 수정할게요.

<div id="map" style="width:1000px;height:800px;"></div>

특정 지점을 마커와 정보를 출력하는 함수를 추가합시다.

기존 마커와 정보 윈도우는 지우고 입력 인자로 들어온 정보로 마커와 정보 윈도우를 표시하는 함수입니다.

var marker;
var infowindow;
function setMarkerOnly(lat,lng,msg){
    var markerPosition  = new kakao.maps.LatLng(lng,lat);
    if(marker != null){
        marker.setMap(null);// 마커 지우기
    }

    // 이동할 위도 경도 위치를 생성합니다 
    marker = new kakao.maps.Marker({
        position: markerPosition
    });
    marker.setMap(map);

    if(infowindow != null){
        infowindow.setMap(null);
    }
    infowindow = new kakao.maps.InfoWindow({
        position: markerPosition,
        content: msg
    });
    infowindow.open(map, marker); 
}

경로를 라인으로 표시하는 함수도 추가합니다.

인자로 위도와 경로를 컬렉션을 받을 것이며 인자를 arguments로 접근할 수 있습니다.

var polyline;
function setLine(){
    var linepath=[];
    var bounds = new kakao.maps.LatLngBounds();
    for(var i=0; i<arguments.length;i+=2){
        linepath.push(new kakao.maps.LatLng(arguments[i],arguments[i+1]));
        bounds.extend(new kakao.maps.LatLng(arguments[i],arguments[i+1]));
    }
    if(polyline != null){
        polyline.setMap(null);
    }
    polyline = new kakao.maps.Polyline({
        path:linepath,
        strokeWeight:5,
        strokeColor:'#FF00FF',
        strokeOpacity:1.0,
        strokeStyle:'solid'
    });
    polyline.setMap(map);
    map.setBounds(bounds);
}

4. 컨트롤 배치

기본으로 제공하는 Form1.cs 파일명을 MainForm.cs로 수정할게요.

그리고 컨트롤은 아래 그림처럼 배치합니다.

컨트롤 배치
컨트롤 배치

5. 출발지와 목적지 선택

btn_start와 btn_end 클릭 이벤트 핸들러를 추가하세요.

두 이벤트 핸들러에서는 지역 검색하여 결과를 ListBox에 보여주는 SeachLocation 함수를 만들어서 사용할게요.

        private void btn_start_Click(object sender, EventArgs e)
        {
            SearchLocation(lbox_start, tbox_start);
        }

        private void btn_end_Click(object sender, EventArgs e)
        {
            SearchLocation(lbox_end, tbox_end);
        }

        private void SearchLocation(ListBox lbox, TextBox tbox)
        {
  
        }

SearchLocation은 경로 탐색 프로젝트에 SelectLocation 코드를 복붙한 후에 수정합시다.

전반적인 코드는 비슷합니다.

결과를 콘솔 화면에 출력하는 것 대신 ListBox.Items에 추가합니다.

그리고 사용자가 선택하는 부분을 생략합니다.

        private void SearchLocation(ListBox lbox, TextBox tbox)
        {
            lbox.Items.Clear();
            string query = tbox.Text;
            string url = "https://dapi.kakao.com/v2/local/search/keyword.json";
            string query_str = string.Format("{0}?query={1}", url, query);
            WebRequest request = WebRequest.Create(query_str);
            request.Headers.Add("Authorization", "KakaoAK 37a3ab31c13f91897f1c983f514dcd16");
            WebResponse response = request.GetResponse();
            Stream stream = response.GetResponseStream();
            StreamReader sr = new StreamReader(stream);
            string content = sr.ReadToEnd();

            JavaScriptSerializer jss = new JavaScriptSerializer();
            dynamic d = jss.Deserialize(content);
            dynamic[] ddoc = d["documents"];
                        
            Locale locale = null;
            foreach (dynamic elem in ddoc)
            {
                string pname = elem["place_name"];
                double lng = double.Parse(elem["x"]);
                double lat = double.Parse(elem["y"]);
                locale = new Locale(pname, lng, lat);
                lbox.Items.Add(locale);
            }
        }

Locale은 클래스로 추가하고 이전 프로젝트의 내용을 복붙합니다.

namespace 경로_찾기
{
    public class Locale
    {
        public string Pname;
        public double Lng { get; }
        public double Lat { get; }
        public Locale(string pname, double lng, double lat)
        {
            Pname = pname;
            Lng = lng;
            Lat = lat;
        }
        public override string ToString()
        {
            return Pname;
        }
    }
}

6. 경로 찾기 구현

btn_search 클릭 이벤트 핸들러를 추가합니다.

시뮬레이션에서 경로 찾기한 부분을 사용하기 위해 경로 컬렉션과 시뮬레이션에서 현재 지점을 나타낼 인덱스를 멤버로 캡슐화합니다.

        List<Locale> locales = new List<Locale>();
        int now;

btn_search 클릭 이벤트 핸들러에서는 먼저 locales를 비우고 lbox_point.Items를 비웁니다.

        private void btn_search_Click(object sender, EventArgs e)
        {
            locales.Clear();
            lbox_point.Items.Clear();

경로 탐색 프로젝트에서 구현한 SearchRoute 메서드의 코드를 복붙한 후에 수정합시다.

시작과 끝은 lbox_start와 lbox_end에서 선택한 항목으로 설정합니다.

            Locale start = lbox_start.SelectedItem as Locale;
            Locale end = lbox_end.SelectedItem as Locale;

웹 요청 및 결과 부분은 대부분 비슷합니다.

차이가 있는 부분을 지역 정보를 콘솔에 출력할 것이 아니라 locales에 추가하는 부분과 lbox_point.Items에 추가하는 것입니다.

        private void btn_search_Click(object sender, EventArgs e)
        {
            locales.Clear();
            lbox_point.Items.Clear();

            Locale start = lbox_start.SelectedItem as Locale;
            Locale end = lbox_end.SelectedItem as Locale;
            string url = "https://apis.openapi.sk.com/tmap/routes";
            string query_str = string.Format(
                "{0}?version=1&startX={1}&startY={2}&endX={3}&endY={4}&startName={5}&endName={6}",
                url, start.Lng, start.Lat, end.Lng, end.Lat, start.Pname, end.Pname);
            WebRequest request = WebRequest.Create(query_str);
            request.Headers.Add("appKey", "akJKr4k7bo2ESeEqvvhq84dbUUTPDUh68Dd0CIVR");
            WebResponse response = request.GetResponse();
            Stream stream = response.GetResponseStream();
            StreamReader reader = new StreamReader(stream);
            string content = reader.ReadToEnd();
            Console.WriteLine(content);
            JavaScriptSerializer jss = new JavaScriptSerializer();
            dynamic dres = jss.Deserialize(content);
            dynamic dfea = dres["features"];
            foreach (dynamic d in dfea)
            {
                if (d["geometry"]["type"] == "Point")
                {
                    dynamic dd = d["geometry"]["coordinates"];
                    string pname = d["properties"]["description"];
                    double lng = (double)dd[0];
                    double lat = (double)dd[1];
                    Locale locale = new Locale(pname, lng, lat);
                    locales.Add(locale);
                    lbox_point.Items.Add(locale);
                }
                if (d["geometry"]["type"] == "LineString")
                {
                    dynamic dd = d["geometry"]["coordinates"];
                    string pname = d["properties"]["name"];
                    bool check = true;
                    foreach (dynamic d3 in dd)
                    {
                        double lng = (double)d3[0];
                        double lat = (double)d3[1];
                        Locale locale = new Locale(pname, lng, lat);
                        locales.Add(locale);
                        if(check)
                        {
                            lbox_point.Items.Add(locale);
                            check = false;
                        }                        
                    }
                }

webbr의 Document 개체를 참조하여 setLine 함수와 setCenter 함수를 호출합니다.

이를 통해 출발지와 목적지 사이의 경로를 지도에 선으로 표시합니다.

그리고 지도의 중심점을 출발지와 목적적의 중간 지점으로 설정합니다.

                object[] objs = new object[locales.Count * 2];
                int i = 0;
                foreach(Locale lo in locales)
                {
                    objs[i] = lo.Lat;
                    i++;
                    objs[i] = lo.Lng;
                    i++;
                }
                HtmlDocument hdoc = webbr.Document;
                hdoc.InvokeScript("setLine", objs);
                object[] objs2 = new object[] {(start.Lat+end.Lat)/2,
                (start.Lng+end.Lng)/2};
                hdoc.InvokeScript("setCenter", objs2);
            }
        }

7. 시뮬레이션 구현

btn_simulation 클릭 이벤트 핸들러를 추가합니다.

핸들러에서는 webbr의 Document 개체를 참조합니다.

시뮬레이션에서는 지도의 Level을 4로 지정할게요.

그리고 timer_simulation을 시작합니다.

        private void btn_simulation_Click(object sender, EventArgs e)
        {
            HtmlDocument hdoc = webbr.Document;
            object[] objs = new object[] { 4 };//level
            hdoc.InvokeScript("setLevel", objs);
            timer_simulation.Start();
        }

timer_simulation의 Tick 이벤트 핸들러를 추가합니다.

만약 시뮬레이션이 끝나지 않았다면 현재 지점을 지도에 보여줍니다.

시뮬레이션이 끝났으면 timer_simulation을 비활성화 시킵니다.

        private void timer_simulation_Tick(object sender, EventArgs e)
        {
            if(now<locales.Count)
            {
                Locale locale = locales[now];
                ViewLocale(locale);
                now++;
            }
            else
            {
                timer_simulation.Enabled = false;
            }
        }

lbox_point의 선택 항목 변경 이벤트 핸들러를 추가합니다.

선택 항목이 없으면 return 합니다.

선택 항목을 Locale 개체로 참조하여 지점 정보를 지도여 보여줍니다.

        private void lbox_point_SelectedIndexChanged(object sender, EventArgs e)
        {
            if(lbox_point.SelectedIndex ==-1)
            {
                return;
            }
            Locale locale = lbox_point.SelectedItem as Locale;
            ViewLocale(locale);
        }

지점 정보를 보여주는 ViewLocale 메서드를 정의합시다.

InfoWindows는 html div 태그로 지점 이름을 출력하게 할게요.

현재 지점으로 지도 중심을 이동시키고 마커(Info Window도 포함)를 지도에 표시합니다.

        private void ViewLocale(Locale locale)
        {
            HtmlDocument hdoc = webbr.Document;
            string msg = string.Format("<div>{0}</div>", locale.Pname);
            object[] objs = new object[] {locale.Lat, locale.Lng};
            hdoc.InvokeScript("setCenter", objs);
            object[] objs2 = new object[] { locale.Lng, locale.Lat,msg };
            hdoc.InvokeScript("setMarkerOnly", objs2);
        }

8. MainForm.cs 소스 코드

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Web.Script.Serialization;
using System.Windows.Forms;

namespace 경로_찾기
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
        }

        private void btn_start_Click(object sender, EventArgs e)
        {
            SearchLocation(lbox_start, tbox_start);
        }

        private void SearchLocation(ListBox lbox, TextBox tbox)
        {
            lbox.Items.Clear();
            string query = tbox.Text;
            string url = "https://dapi.kakao.com/v2/local/search/keyword.json";
            string query_str = string.Format("{0}?query={1}", url, query);
            WebRequest request = WebRequest.Create(query_str);
            request.Headers.Add("Authorization", "KakaoAK 37a3ab31c13f91897f1c983f514dcd16");
            WebResponse response = request.GetResponse();
            Stream stream = response.GetResponseStream();
            StreamReader sr = new StreamReader(stream);
            string content = sr.ReadToEnd();

            JavaScriptSerializer jss = new JavaScriptSerializer();
            dynamic d = jss.Deserialize(content);
            dynamic[] ddoc = d["documents"];
                        
            Locale locale = null;
            foreach (dynamic elem in ddoc)
            {
                string pname = elem["place_name"];
                double lng = double.Parse(elem["x"]);
                double lat = double.Parse(elem["y"]);
                locale = new Locale(pname, lng, lat);
                lbox.Items.Add(locale);
            }
        }

        private void btn_end_Click(object sender, EventArgs e)
        {
            SearchLocation(lbox_end, tbox_end);
        }

        List locales = new List();
        int now;

        private void btn_search_Click(object sender, EventArgs e)
        {
            locales.Clear();
            lbox_point.Items.Clear();

            Locale start = lbox_start.SelectedItem as Locale;
            Locale end = lbox_end.SelectedItem as Locale;
            string url = "https://apis.openapi.sk.com/tmap/routes";
            string query_str = string.Format(
                "{0}?version=1&startX={1}&startY={2}&endX={3}&endY={4}&startName={5}&endName={6}",
                url, start.Lng, start.Lat, end.Lng, end.Lat, start.Pname, end.Pname);
            WebRequest request = WebRequest.Create(query_str);
            request.Headers.Add("appKey", "akJKr4k7bo2ESeEqvvhq84dbUUTPDUh68Dd0CIVR");
            WebResponse response = request.GetResponse();
            Stream stream = response.GetResponseStream();
            StreamReader reader = new StreamReader(stream);
            string content = reader.ReadToEnd();
            Console.WriteLine(content);
            JavaScriptSerializer jss = new JavaScriptSerializer();
            dynamic dres = jss.Deserialize(content);
            dynamic dfea = dres["features"];
            foreach (dynamic d in dfea)
            {
                if (d["geometry"]["type"] == "Point")
                {
                    dynamic dd = d["geometry"]["coordinates"];
                    string pname = d["properties"]["description"];
                    double lng = (double)dd[0];
                    double lat = (double)dd[1];
                    Locale locale = new Locale(pname, lng, lat);
                    locales.Add(locale);
                    lbox_point.Items.Add(locale);
                }
                if (d["geometry"]["type"] == "LineString")
                {
                    dynamic dd = d["geometry"]["coordinates"];
                    string pname = d["properties"]["name"];
                    bool check = true;
                    foreach (dynamic d3 in dd)
                    {
                        double lng = (double)d3[0];
                        double lat = (double)d3[1];
                        Locale locale = new Locale(pname, lng, lat);
                        locales.Add(locale);
                        if(check)
                        {
                            lbox_point.Items.Add(locale);
                            check = false;
                        }                        
                    }
                }

                object[] objs = new object[locales.Count * 2];
                int i = 0;
                foreach(Locale lo in locales)
                {
                    objs[i] = lo.Lat;
                    i++;
                    objs[i] = lo.Lng;
                    i++;
                }
                HtmlDocument hdoc = webbr.Document;
                hdoc.InvokeScript("setLine", objs);
                object[] objs2 = new object[] {(start.Lat+end.Lat)/2,
                (start.Lng+end.Lng)/2};
                hdoc.InvokeScript("setCenter", objs2);
            }
        }

        private void btn_simulation_Click(object sender, EventArgs e)
        {
            HtmlDocument hdoc = webbr.Document;
            object[] objs = new object[] { 4 };//level
            hdoc.InvokeScript("setLevel", objs);
            timer_simulation.Start();
        }

        private void timer_simulation_Tick(object sender, EventArgs e)
        {
            if(now<locales.count) {="" locale="" viewlocale(locale);="" now++;="" }="" else="" timer_simulation.enabled="false;" private="" void="" lbox_point_selectedindexchanged(object="" sender,="" eventargs="" e)="" if(lbox_point.selectedindex="=-1)" return;="" as="" locale;="" viewlocale(locale="" locale)="" htmldocument="" hdoc="webbr.Document;" string="" msg="string.Format("<div">{0}", locale.Pname);
            object[] objs = new object[] {locale.Lat, locale.Lng};
            hdoc.InvokeScript("setCenter", objs);
            object[] objs2 = new object[] { locale.Lng, locale.Lat,msg };
            hdoc.InvokeScript("setMarkerOnly", objs2);
        }
    }
}
</locales.count)>

이제 모든 프로젝트 구현을 마감합니다.

모두 수고하였습니다.