카테고리 없음

kakao map에 gpx 올리기

giljabi 2024. 12. 12. 21:54

https://github.com/giljabi/giljabi-start

 

GitHub - giljabi/giljabi-start

Contribute to giljabi/giljabi-start development by creating an account on GitHub.

github.com

 

버전 업하면서 gpx 파일을 지도에 올리는 것을 궁금해하는 분들이 많아 작성하게 되었습니다. gpx 부분만 최대한 간단하게 작성하였고, github을 보시면 어렵지 않게 볼 수 있습니다.

 

카카오맵 API는 https://apis.map.kakao.com/web/ 예제를 보시고 기본 지도는 사용할 수 있다는 전제로 설명합니다. 지도에서 사용할 app key는 https://developers.kakao.com/ 등록하시면 됩니다.

 

Day 1

https://github.com/giljabi/giljabi-start/tree/main/src/main/resources/static/day1

 

Project 만들기

기본 프로젝트 설정

* giljabi 프로젝트는 spring boot 2.6.x를 사용합니다. 하지만 Spring boot 프로젝트는 항상 최신 버전으로 만들기 때문에 버전은 프로젝트 생성 후 변경할 예정이므로 기본값으로 나오는 버전을 선택합니다.
* lombok, Spring web(tomcat), DB(postgresql)

 

 

pom.xml 수정

JDK: 11, Spring boot: 2.6.1, 현재 사용하지 않는 부분은 삭제하고 위 2개만 수정하고 "maven project reload"합니다.

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>giljabi-start</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>giljabi-start</name>
    <description>giljabi-start</description>
    <properties>
        <java.version>11</java.version>
    </properties>

 

 

day1/mygpx.html 작성

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>길잡이 시작</title>
</head>
<body>
Hello, Giljabi.
</body>
</html>

 

파일위치는 아래와 같습니다.

 

 

 

카카오맵 API key

카카오맵을 사용하려면 app key 먼저 받아서 준비 후 진행해야 합니다. app key를 가지고 지도를 초기화하는 것은 카카오맵에 설명이 잘 되어 있으니 생략합니다.

https://developers.kakao.com/console/app

 

 

카카오맵 초기화면

만들고자 하는 화면을 정의합니다. gpx파일을 선택해서 브라우저에 올리는 버튼, 그리고 gpx 파일의 간단한 속성을 보여주는 정보입니다.

 

 

화면 초기화

day1/mygpx.js

중요한 부분은 파일을 읽어서 xml를 파싱하는 부분입니다. wpt, trkseg, trkpt를 읽어서 변수에 저장하면 절반은 왔습니다.

let gMap;

function initMap() {
    let mapContainer = document.getElementById('map');
    let mapOption = {
        center: new kakao.maps.LatLng(37.56683546665817, 126.9786607449023),	//지도의 중심위치
        level: 12
    };
    gMap = new kakao.maps.Map(mapContainer, mapOption);
    gMap.addControl(new kakao.maps.MapTypeControl(), kakao.maps.ControlPosition.TOPRIGHT);
}


function clickGpxLoadButton() {
    $('#loadButton').on('click', function () {
        const file = $('#fileUpload')[0].files[0];

        if (!file) {
            alert('Please select a GPX file first.');
            return;
        }

        const reader = new FileReader();
        reader.onload = function (event) {
            const fileContent = event.target.result;
            let readXmlfile = $($.parseXML(fileContent.replace(/&/g, '&amp;')));
            const waypoints = readXmlfile.find('wpt');
            const trkseg = readXmlfile.find('trkseg');
            const trkpt = readXmlfile.find('trkpt');

            $('#gpxOutput').text(`waypoint:${waypoints.length}, 
            trkseg:${trkseg.length}, trkpt:${trkpt.length}`);
        };
        reader.readAsText(file);
    });
}

$(document).ready(function () {
    initMap();
    clickGpxLoadButton();
});

 

 

Day2

https://github.com/giljabi/giljabi-start/tree/main/src/main/resources/static/day2

gpx Object

gpx 파일은 xml 파일이고, 명세는 GPX 1.1 Schema Documentation(https://www.topografix.com/GPX/1/1/)에 있습니다.

Element: gpx
Complex Type: gpxType
Complex Type: metadataType
Complex Type: wptType
Complex Type: rteType
Complex Type: trkType
Complex Type: extensionsType
Complex Type: trksegType
Complex Type: copyrightType
Complex Type: linkType
Complex Type: emailType
Complex Type: personType
Complex Type: ptType
Complex Type: ptsegType
Complex Type: boundsType
Simple Type: latitudeType
Simple Type: longitudeType
Simple Type: degreesType
Simple Type: fixType
Simple Type: dgpsStationType

 

핵심적인 부분만 사용을 하면 많지 않습니다. metadata, wpt, trkseg, trkpt 대략 이 정도만 사용합니다. 

 

gpx 파일 내용

trkpt에서 dist는 원래 명세에는 없습니다만, 거리를 계산하기 위해 편의상 추가하였으며 garmin 기기에서 사용 시 문제는 없습니다. 

<?xml version="1.0" encoding="UTF-8"?>
<gpx creator="giljabi" version="1.1"
 xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/11.xsd"
 xmlns:ns3="http://www.garmin.com/xmlschemas/TrackPointExtension/v1"
 xmlns="http://www.topografix.com/GPX/1/1"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ns2="http://www.garmin.com/xmlschemas/GpxExtensions/v3">
<metadata>
    <name>hwaaksan</name>
    <link href="http://www.giljabi.kr" />
    <desc>giljabi</desc>
    <copyright>giljabi.kr</copyright>
    <speed>2</speed>
    <time>2024-05-29T00:00:00.000Z</time>
</metadata>
<wpt lat="37.957928" lon="127.472581">
    <name>들머리</name>
    <sym>Generic</sym>
</wpt>
<wpt lat="37.958195" lon="127.472373">
    <name>날머리</name>
    <sym>Generic</sym>
</wpt>
<trk>
    <trkseg>
        <trkpt lat="37.957961" lon="127.472555">
            <ele>256</ele>
            <time>2024-05-29T00:00:00.000Z</time>
            <dist>0</dist>
        </trkpt>
        <trkpt lat="37.957961" lon="127.472569">
            <ele>254</ele>
            <time>2024-05-29T00:00:02.000Z</time>
            <dist>1.23</dist>
        </trkpt>
    </trkseg>
</trk>
</gpx>

 

Object 정의

브라우저에 올리기 위해 각 object를 정의합니다.

function wpt(lat, lon, ele, name, sym) {
    this.lat = Number(parseFloat(lat).toFixed(6)); // toFixed를 사용한 뒤 문자열로 처리
    this.lon = Number(parseFloat(lon).toFixed(6));
    this.ele = isNaN(ele) ? 0.00 : Number(parseFloat(ele).toFixed(2));
    this.name = name;
    this.sym = sym;
}

function trkpt(lat, lon, ele, time, dist) {
    this.lat = Number(parseFloat(lat).toFixed(6)); //소수점 이하 6자리사용, 정밀도가 더 높아도 데이터만 크고 이득이 없음
    this.lon = Number(parseFloat(lon).toFixed(6));
    this.ele = isNaN(ele) ? 0.00 : Number(parseFloat(ele).toFixed(2));
    this.time = time;
    this.dist = isNaN(dist) ? 0.00 : Number(parseFloat(dist.toFixed(2)));  //garmin gpx 포맷에는 없으나 거리정보를 추가해서 사용
}

 

 

http://localhost:8080/day2/mygpx.html

 

 

Day3

https://github.com/giljabi/giljabi-start/tree/main/src/main/resources/static/day3

gpx save

gpx 파일에서는 위치정보는 필수이고, 높이, 시간은 선택, 거리(dist)는 추가정보입니다. 각 포인트간의 거리, 이동속도를 이용하여 데이터를 만들고 gpx파일로 저장합니다.

 

전체 소요시간을 계산하려면 경로에 있는 포인트간의 거리를 알아야 합니다.
* gpt에서 알려준 vincentyDistance, getDistance를 이용하여 마라톤 코스를 비교하면 거리차이는 무시해도 될 수준이므로 간편한 공식을 사용합니다. 서울마라톤 풀코스(trkpt:4717개)를 기준으로 28.8M 정도 차이가 나는데 이는 0.07% 차이입니다. 어느 것도 완전한 것은 없기에 간단한 공식(getDistance)을 사용합니다.

 

|vincentyDistance | getDistance           |
|------------------|-------------------|
| 42231.11934530 | 42202.23066659| 

 

function deg2rad(deg) {
    return deg * (Math.PI / 180);
}

function getDistance(fromPoint, toPoint) {
    let R = 6371e3;
    let dLat = deg2rad(toPoint.getLat() - fromPoint.getLat());
    let dLon = deg2rad(toPoint.getLng() - fromPoint.getLng());
    let a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
        Math.cos(deg2rad(fromPoint.getLat())) * Math.cos(deg2rad(toPoint.getLat())) *
        Math.sin(dLon / 2) * Math.sin(dLon / 2);
    let c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return R * c;
}

 

시간 계산

거리를 알면 이동속도를 이용하여 각 경로마다 예상시간을 계산할 수 있습니다. 거리와 시간을 계산해서 object로 만들고 배열로 저장합니다.

function getMoveTimeAndDistance() {
    let startTime = setBaseTimeToToday(new Date('2024-01-01T00:00:00Z'));   //시간 초기화
    let speed = Number($('#speed').val());
    $.each(gTrkpointArray, function (index) {
        if (index == 0) { // index > 0 부터 거리 계산
            // index = 0, 초기값 설정
            gTrkpointArray[index].dist = 0;
            gTrkpointArray[index].time = startTime.toISOString();
        } else {
            let distance = getDistance(gTrkpointArray[index - 1].lat, gTrkpointArray[index - 1].lon,
                gTrkpointArray[index].lat, gTrkpointArray[index].lon);
            //let distance = getDistance(gTrkpointPolyline[index - 1], gTrkpointPolyline[index]);
            let duration = (distance / (speed * 1000 / 3600)); // m/s로 변환
            startTime.setSeconds(startTime.getSeconds() + duration);

            gTrkpointArray[index].dist = Number((Number(gTrkpointArray[index - 1].dist || 0) + distance).toFixed(2));
            gTrkpointArray[index].time = startTime.toISOString();
            //console.log(index + ', ' + distance + ',' + duration + ',' + gTrkpointArray[index].time + ', '+ gTrkpointArray[index].dist +','+ gTrkpointArray[index].lat);
        }
    });
}

 

저장

저장버튼을 클릭하면 변수에 저장된 내용을 파일로 저장합니다.

function clickGpxSaveButton() {
    $('#saveButton').on('click', function () {
        getMoveTimeAndDistance();   //gpx 저장에 필요한 정보

        const fileName = $('#fileUpload')[0].files[0].name;  //필요시 경로명을 입력받아서 사용
        const gpxName = fileName.split('.');
        const gpxFileData = makeGpxData(gpxName[0], Number($('#speed').val()), gWptArray, gTrkpointArray);

        saveAs(new Blob([gpxFileData], {
            type: "application/vnd.garmin.tcx+xml"
        }), gpxName[0] + '.gpx'); //지금은 gpx만 사용

        console.log('gpx file saved')
    });
}

 

save gpx format

gpx로 저장하는 파일구조입니다. 만약 tcx로 저장한다면 동일한 방법으로 하면됩니다.

function makeGpxData(filename, speed, wpt, trkpt) {
    let xmlDataParts = [];
    xmlDataParts.push('<?xml version="1.0" encoding="UTF-8"?>');
    xmlDataParts.push('<gpx creator="giljabi" version="1.1"');
    xmlDataParts.push(' xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/11.xsd"');
    xmlDataParts.push(' xmlns:ns3="http://www.garmin.com/xmlschemas/TrackPointExtension/v1"');
    xmlDataParts.push(' xmlns="http://www.topografix.com/GPX/1/1"');
    xmlDataParts.push(' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' +
        'xmlns:ns2="http://www.garmin.com/xmlschemas/GpxExtensions/v3">');

    xmlDataParts.push(`<metadata>`);
    xmlDataParts.push(` <name>${filename}</name>`);
    xmlDataParts.push(` <link href="https://giljabi.kr" />`);
    xmlDataParts.push(` <desc>giljabi</desc>`);
    xmlDataParts.push(` <copyright>giljabi.kr</copyright>`);
    xmlDataParts.push(` <speed>${speed}</speed>`);
    xmlDataParts.push(` <time>${trkpt[0].time}</time>`);
    xmlDataParts.push(`</metadata>`);

    wpt.forEach(w => {
        xmlDataParts.push(`<wpt lat="${w.lat}" lon="${w.lon}">`);
        xmlDataParts.push(` <name>${w.name}</name>`);
        xmlDataParts.push(` <sym>${w.sym}</sym>`);
        xmlDataParts.push(`</wpt>`);
    });

    xmlDataParts.push(`<trk>`);
    xmlDataParts.push(` <trkseg>`);
    trkpt.forEach(pt => {
        xmlDataParts.push(` <trkpt lat="${pt.lat}" lon="${pt.lon}">`);
        xmlDataParts.push(` <ele>${pt.ele}</ele>`);
        xmlDataParts.push(` <time>${pt.time}</time>`);
        xmlDataParts.push(` <dist>${pt.dist}</dist>`);   //기본 속성이 없으나 확장속성을 사용하지 않고 사용
        xmlDataParts.push(` </trkpt>`);
    });
    xmlDataParts.push(` </trkseg>`);
    xmlDataParts.push(`</trk>`);
    xmlDataParts.push(`</gpx>`);
    return xmlDataParts.join(NEWLINE);
}

 

최종 모습입니다. 이동 속도가 필요한 이유는 포인트 이동시간 그리고 최종 소요시간을 계산하는 데 사용됩니다. 에러/예외 처리는 거의 없으니 필요에 따라 추가해서 사용하면 됩니다. 

끝.