kakao map에 gpx 올리기
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, '&')));
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);
}
최종 모습입니다. 이동 속도가 필요한 이유는 포인트 이동시간 그리고 최종 소요시간을 계산하는 데 사용됩니다. 에러/예외 처리는 거의 없으니 필요에 따라 추가해서 사용하면 됩니다.
끝.