참고
https://www.youtube.com/watch?v=jTmbYiK9OcI
https://apis.map.kakao.com/web/documentation/#Polygon
지도의 필수요소라고 할 수 있는 '길찾기' 기능이 있는데, 지도상에서 직접 클릭을 통해 직선을 그어 경로를 생성하고 삭제할 수 있도록 구현을 해보았다.
Vue.js 기반 프로젝트에서 Kakao Map api에서 제공하는 Polyline을 사용해서 경로를 생성한다. 생성할 경로 정보를 담당할 클래스 Segment를 작성해서 경로 생성 및 수정 삭제등의 기능을 추가한다.
새로운 경로
버튼과 지도를 화면에 채운다.
새로운 경로
버튼을 클릭하면 지도에 지점들을 다 클릭할 수 있도록 할 것이고, 지점들의 선택이 완료가 된다면 종료
버튼을 눌러 다시 첫번째 화면으로 돌아갈 수 있도록 한다.
<template>
<div id="app">
<div class="path-list">
<h3>경로</h3>
<button @click="processNewPath()">새로운 경로</button>
</div>
<div class="map-wrapper" ref="kakaomap">
<button class="btn-commit-seg" v-if="activeSegment" @click="commitPath()">
종료</button
><!-- 경로 종료할 때 버튼 -->
</div>
</div>
</template>
Kakao Map API 문서에서 생성할 경로 정보를 담당할 클래스 Segment를 작성해서 경로 생성할 수 있다.
//segment.js
/*
* 경로를 나타내는 클래스
*/
class Segment {
/**
*
* @param {kakao.maps.Map} mapInstance 지도 인스턴스
*/
constructor(mapInstance, props) {
this.map = mapInstance;
}
}
let currentSegment;
const startSegment = (mapInstance) => {
currentSegment = new Segment(mapInstance);
return currentSegment;
};
export default startSegment;
data() {
return {
mapInstance: null, //지도 인스턴스
activeSegment: null, //종료 버튼 활성화
}
},
mounted() {
//init map here
var container = this.$refs.kakaomap;
var options = {
center: new window.kakao.maps.LatLng(33.450701, 126.570667),
level: 3,
};
//지도 생성 및 객체 리턴
this.mapInstance = new window.kakao.maps.Map(container, options);
},
methods: {
processNewPath() {
console.log("[new path] start");
//지도 객체 전달
this.activeSegment = startSegment(this.mapInstance);
//startSegment 객체 생성 -> activeSegment 존재 -> 확인 버튼 활성화
},
},
마운트 될 때 지도를 생성하여 화면에 렌더링할 수 있도록 하고,
새로운 경로
버튼 클릭 이벤트가 발생하면 processNewPath
메서드를 통해 Segment 객체가 생성이 되고 activeSegment 로 저장하여 v-if 바인딩을 통해 확인
버튼이 활성화되게 실행한다.
/*
* 경로를 나타내는 클래스
*/
class Segment {
/**
*
* @param {kakao.maps.Map} mapInstance 지도 인스턴스
*/
constructor(mapInstance, props) {
this.map = mapInstance;
this.done = false; //+추가) 경로 작성 완료 전
this.points = []; //+추가) list for LatLng
this.poly = new window.kakao.maps.Polyline({
map: this.map,
path: [],
strokeWeight: 2,
strokeColor: "#FF00FF",
strokeOpacity: 0.8,
strokeStyle: "solid", //실선
}); //+추가) 폴리라인 객체 생성
this.listeners = {};
this.installListners(); //+추가) 클릭 이벤트 등록
}
installListners() {
//클릭 리스너 등록
const adder = (e) => {
console.log("[pos]", e.latLng);
this.points.push(e.latLng); //각 점을 등록
this.render();
};
this.listeners.click = adder;
window.kakao.maps.event.addListener(this.map, "click", adder);
}
render() {
//경로를 그림
this.poly.setPath(this.points);
}
//경로 추가 메서드
commit() {
//경로 작성 완료
this.done = true;
//리스너 해제해야함
window.kakao.maps.event.removeListener(
this.map,
"click",
this.listeners.click
);
//커서를 가져오는 API가 없기에
this.map.setCursor(DEFAULT_KAKAOMAP_CURSOR);
console.log("해제 완료");
}
}
Segment 생성자에서 경로 작성 완료 전의 의미라는 done 변수를 false 로 잡아두고, commit
메서드에서는 완료로 처리되어야 하기 때문에 true 로 변경해준다.
새로운 경로 버튼 클릭을 하고 맵상에서 위치를 클릭하면 각각의 해당 lat 와 lng 가 출력되는 것을 볼 수 있다. 이를 위도와 경로의 배열을 나타내는 points 변수에 저장시킨다.
지도 클릭 이벤트를 등록하려면 아래와 같은 이벤트와 콜백함수를 추가시켜준다.
주어진 객체로 폴리라인을 생성한다. 옵션에서 map 과 path 는 각각 지도와 선택된 지점들의 배열을 가리킨다.
설정한 polyline 을 이용해 경로를 그리기 위해 setPath
메서드를 활용한다.
commit 메서드에서는 installListners 메서드와는 다르게 클릭 리스너를 해제시킨다.
methods: {
commitPath() {
this.activeSegment.commit();
this.segments.push(this.activeSegment);
this.activeSegment = null; //버튼 사라지게 함
},
}
data() {
return {
mapInstance: null,
activeSegment: null,
segments: [], //+추가) list of pathes
}
}
경로들의 목록을 segments 변수로 저장한다.
methods: {
processNewPath() {
console.log("[new path] start");
//지도 객체 전달
this.activeSegment = startSegment(this.mapInstance);
},
commitPath() {
this.activeSegment.commit();
this.segments.push(this.activeSegment); //+추가)
this.activeSegment = null; //버튼 사라지게 함
},
}
commitPath 메서드에서 activeSegment을 null하기 전에 segments 배열에 activeSegment 들을 추가시킨다.
<template>
<div id="app">
<div class="path-list">
<h3>경로</h3>
<button @click="processNewPath()">새로운 경로</button>
<!-- 경로 목록 렌더링 -->
<div class="list-of-seg">
<div class="segment" v-for="(seg, index) in segments" :key="index">
<h4>{{ seg.name }}</h4>
<!-- 경로 이름 필요함 -->
</div>
</div>
<!-- 경로 목록 렌더링 -->
</div>
<div class="map-wrapper" ref="kakaomap">
<button class="btn-commit-seg" v-if="activeSegment" @click="commitPath()">
종료</button
><!-- 경로 종료할 때 버튼 -->
</div>
</div>
</template>
const DEFAULT_KAKAOMAP_CURSOR =
"url("http://t1.daumcdn.net/mapjsapi/images/2x/cursor/openhand.cur.ico") 7 5, url("http://t1.daumcdn.net/mapjsapi/images/2x/cursor/openhand.cur.ico"), default";
/*
* 경로를 나타내는 클래스
*/
class Segment {
/**
*
* @param {kakao.maps.Map} mapInstance 지도 인스턴스
* @param {object} props 경로 메타 정보들(이름 거리 등..)
*/
constructor(mapInstance, props) {
this.map = mapInstance;
this.done = false;
this.points = []; //list for LatLng
this.poly = new window.kakao.maps.Polyline({
map: this.map,
path: [],
strokeWeight: 2,
strokeColor: "#FF00FF",
strokeOpacity: 0.8,
strokeStyle: "solid", //실선
});
this.listeners = {};
this.props = props || {};
this.installListners();
}
get name() {
return this.props.name || "NO NAME";
}
installListners() {
//클릭 리스너 등록
const adder = (e) => {
console.log("[pos]", e.latLng);
this.points.push(e.latLng); //각 점을 등록
this.render();
};
// this.listeners.click = { click: null };
this.listeners.click = adder;
window.kakao.maps.event.addListener(this.map, "click", adder);
//+추가) 마우스 커서 설정
this.map.setCursor("crosshair");
}
commit() {
//경로 작성 완료
this.done = true;
//리스너 해제해야함
window.kakao.maps.event.removeListener(
this.map,
"click",
this.listeners.click
);
//+추가) 커서를 가져오는 API가 없기에
this.map.setCursor(DEFAULT_KAKAOMAP_CURSOR);
console.log("해제 완료");
}
}
클릭 리스너가 진행중일 때, 마우스 커서가 십자 corsshair 가 되도록 변경한다.
경로 종료 후 마우스 커서를 원위치로 되돌리기 위해서 임의로 마우스 커서의 위치를 따와 DEFAULT 변수로 만들어 commit 메서드에서 setCuror 재설정한다.
<template>
<div id="app">
<div class="path-list">
<h3>경로</h3>
<button @click="processNewPath()">새로운 경로</button>
<div class="list-of-seg">
<div class="segment" v-for="(seg, index) in segments" :key="index">
<h4>{{ seg.name }}</h4>
<!-- 경로 이름 필요함 -->
</div>
</div>
</div>
<div class="map-wrapper" ref="kakaomap">
<button class="btn-commit-seg" v-if="activeSegment" @click="commitPath()">
종료</button
><!-- 경로 종료할 때 버튼 -->
</div>
</div>
</template>
<script>
import startSegment from "./segment";
export default {
name: "App",
components: {
},
data() {
return {
mapInstance: null,
activeSegment: null,
segments: [], //list of pathes
};
},
mounted() {
//init map here
var container = this.$refs.kakaomap;
var options = {
center: new window.kakao.maps.LatLng(33.450701, 126.570667),
level: 3,
};
//지도 생성 및 객체 리턴
this.mapInstance = new window.kakao.maps.Map(container, options);
},
methods: {
processNewPath() {
console.log("[new path] start");
//지도 객체 전달
this.activeSegment = startSegment(this.mapInstance);
},
commitPath() {
this.activeSegment.commit();
this.segments.push(this.activeSegment);
this.activeSegment = null; //버튼 사라지게 함
},
},
};
</script>
<style lang="scss">
html,
body {
height: 100%;
margin: 0;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
.path-list {
width: 240px;
}
.map-wrapper {
flex: 1 1 auto;
.btn-commit-seg {
position: absolute;
top: 5px;
left: 5px;
z-index: 1000;
}
}
}
</style>
const DEFAULT_KAKAOMAP_CURSOR =
"url("http://t1.daumcdn.net/mapjsapi/images/2x/cursor/openhand.cur.ico") 7 5, url("http://t1.daumcdn.net/mapjsapi/images/2x/cursor/openhand.cur.ico"), default";
/*
* 경로를 나타내는 클래스
*/
class Segment {
/**
*
* @param {kakao.maps.Map} mapInstance 지도 인스턴스
* @param {object} props 경로 메타 정보들(이름 거리 등..)
*/
constructor(mapInstance, props) {
this.map = mapInstance;
this.done = false;
this.points = []; //list for LatLng
this.poly = new window.kakao.maps.Polyline({
map: this.map,
path: [],
strokeWeight: 2,
strokeColor: "#FF00FF",
strokeOpacity: 0.8,
strokeStyle: "solid", //실선
});
this.listeners = {};
this.props = props || {};
this.installListners();
}
get name() {
return this.props.name || "NO NAME";
}
installListners() {
//클릭 리스너 등록
const adder = (e) => {
console.log("[pos]", e.latLng);
this.points.push(e.latLng); //각 점을 등록
this.render();
};
// this.listeners.click = { click: null };
this.listeners.click = adder;
window.kakao.maps.event.addListener(this.map, "click", adder);
this.map.setCursor("crosshair");
}
render() {
//경로를 그림
this.poly.setPath(this.points);
}
commit() {
//경로 작성 완료
this.done = true;
//리스너 해제해야함
window.kakao.maps.event.removeListener(
this.map,
"click",
this.listeners.click
);
//커서를 가져오는 API가 없기에
this.map.setCursor(DEFAULT_KAKAOMAP_CURSOR);
console.log("해제 완료");
}
}
let currentSegment;
const startSegment = (mapInstance) => {
currentSegment = new Segment(mapInstance);
return currentSegment;
};
export default startSegment;