[Vue.js] Implementing Vue Kakao Maps 05 - Fixing the Polyline 03 route

[Vue.js] Implementing Vue Kakao Maps 05 - Modifying the Polyline 03 route

In a Vue.js-based project, edit a route using the Polyline provided by the Kakao Maps API.

  1. click to select the created route
  2. create markers for each location that makes up the route
  3. drag the markers to modify the route

implement the state classes responsible for creating and editing route information.

  • NewState - responsible for creating a new route
  • NormalState - the default state after a route has been created
  • EditState - responsible for route editing functionality
StateTransition
NEW |] [Done Button] |Β V NORMAL ---> [Select path] ----> EDIT ^ | | | +--------- [edit]----------+

In Section 1, attach the source code.

Section 2 describes the code.

1. source code

added seg-state.js from previous implementation (path generation)

TXT
  • index.html - no modifications
  • main.js - no modifications
  • App.vue - Application Root Component (modified)
  • segment.js - Managing paths using Polyline (Modified)
  • seg-state.js - Classes for managing state (+)

1.1. seg-state.js (added)

this is responsible for connecting and disconnecting various listeners to the map based on the three states (NEW, NORMAL, and EDIT). It also controls the appearance of the map or route reflecting each state (such as the color thickness of the lines).

seg-state.js
const { addListener, removeListener } = window.kakao.maps.event; /** /** The * path creation state * - Switch to NORMAL state on completion */** /** /** /** /** /** /** /** / class NewState { constructor() { this.name = "NEW"; this.segments = []; this.option = {}; this.listeners = { click: (seg, latLng) => { seg.addPoint(latLng); seg.render(); }, }; } ready(seg) { const { map } = seg; map.setCursor("crosshair"); const listeners = { click: (e) => this.listeners.click(seg, e.latLng), }; addListener(map, "click", listeners.click); this.segments.push({ seg, listeners }); } release(seg) { // release state: listener removed const index = this.segments.findIndex((elem) => elem.seg === seg); if (index >= 0) { const elem = this.segments[index]; const { seg, listeners } = elem; removeListener(seg.map, "click", listeners.click); this.segments.splice(index, 1); // removed } } } class NormalState { constructor() { this.name = "NORMAL"; this.segments = []; this.options = { HOVER: { strokeWidth: 5, strokeColor: "#FF00FF", }, NORMAL: { strokeWidth: 3, strokeColor: "blue", }, }; } ready(seg) { // normal state const { poly } = seg; const listeners = { mouseover: () => { poly.setOptions(this.options.HOVER); }, mouseout: () => { poly.setOptions(this.options.NORMAL); }, click: () => { // console.log("[Switch to edit state"); seg.setState("EDIT"); }, }; seg.poly.setOptions(this.options.NORMAL); addListener(poly, "mouseover", listeners.mouseover); addListener(poly, "mouseout", listeners.mouseout); addListener(poly, "click", listeners.click); this.segments.push({ seg, listeners }); } release(seg) { // Releases the listener const index = this.segments.findIndex((elem) => elem.seg === seg); if (index >= 0) { const elem = this.segments[index]; const { seg, listeners } = elem; const { poly } = seg; removeListener(poly, "mouseover", listeners.mouseover); removeListener(poly, "mouseout", listeners.mouseout); removeListener(poly, "click", listeners.click); this.segments.splice(index, 1); // removed } } } /** * edit status - path position modified * return to normal after completion */** /** Return to normal after completion class EditState { constructor() { this.name = "EDIT"; this.segments = []; } ready(seg) { if (this.segments.length === 1) { // we only want to allow one path to edit // clear the edit state if it already exists this.segments[0].seg.setState("NORMAL"); } // create markers const markers = seg.points.map((latLng, index) => { const marker = new window.kakao.maps.Marker({ map: seg.map, position: latLng, draggable: true, }); // addListener(marker, 'dragstart', () => {{}}) addListener(marker, "dragend", () => { const latLng = marker.getPosition(); seg.points[marker.index] = latLng; seg.render(); }); marker.index = index; return marker; }); this.segments.push({ seg, markers }); } release(seg) { // Releases the listener const index = this.segments.findIndex((elem) => elem.seg === seg); if (index >= 0) { const elem = this.segments[index]; const { markers } = elem; markers.forEach((marker) => { marker.setMap(null); // clear marker }); this.segments.splice(index, 1); } } } export { NewState, NormalState, EditState };

1.2. segment.js (modified)

we leave the state management of the route to three state classes.

segment.js
import Vue from "vue"; import { NewState, NormalState, EditState } from "./seg-state"; const DEFAULT_KAKAOMAP_CURSOR = { return 'url("http://t1.daumcdn.net/mapjsapi/images/cursor/openhand.cur.ico") 7 5, url("http://t1.daumcdn.net/mapjsapi/images/cursor/openhand.cur.ico"), default'; const segState = Vue.observable({ target: null }); /** } * NEW (path creation state) -- on completion --> NORMAL ----> EDIT * ^|] * +-----completed---+ */] const stateMap = new Map(); stateMap.set("NEW", new NewState()); stateMap.set("NORMAL", new NormalState()); stateMap.set("EDIT", new EditState()); // let currentSegment; const startSegment = (mapInstance) => { segState.target = new Segment(mapInstance); segState.target.setState("NEW"); }; /** * classes to represent paths */** Classes that represent paths class Segment { /** * * @param {kakao.maps.Map} mapInstance map instance * @param {object} props route meta information (name distance etc...) */** @param 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: [], }); // list for LatLng this.listeners = { click: null }; this.props = props || {}; } get name() { return this.props.name || "NO NAME"; } get state() { return (this.stateHandler && this.stateHandler.name) || null; } render() { // plot the path this.poly.setPath(this.points); } addPoint(latLng) { this.points.push(latLng); } setState(stateName) { const prevStateHandler = this.stateHandler; if (prevStateHanlder) { // release the state prevStateHanlder.release(this); } } const stateHandler = stateMap.get(stateName); this.stateHandler = stateHandler; if (this.stateHandler) { this.stateHandler.ready(this); } if (this.state === "NORMAL") { segState.target = null; } else { segState.target = this; } } commit() { // Finish building the route this.done = true; // There is no API to get the cursor. // we just get it and restore it like this this.map.setCursor(DEFAULT_KAKAOMAP_CURSOR); console.log("[Dismissed]"); this.setState("NORMAL"); } dispose() { console.log("Path removed"); this.commit(); // return cursor this.poly.setMap(null); } } export { segState }; export default startSegment;
  • the installListener() function moves to NormalState
  • Use Vue.observable(..) to change the state of the path to a reactive object. the screen immediately updates to reflect the state of the reactive object.
  • export the reactive object, segState.

1.3. App.vue (Modified)

modify the code to reflect state changes in the path via the segState exposed in segment.js to the screen

App.vue
<template> <div id="app <div id="app"> <div class="path-list <div class="path-list"> <h3>Path</h3> <h3>Path</h3> <div class="path-list"> <h3>Paths</h3> <button @click="processNewPath()">New path</button> <div class="list-of-seg"> <div class="segment" v-for="(seg, index) in segments" :key="index"> <h4>{{ seg.name }}</h4> <button @click="deletePath(seg, index)">Delete</button> </div> </div> </div> <div class="map-wrapper" ref="kakaomap"> <div class="map-control" v-if="activeSegment"> <template v-if="activeSegment.state === 'NEW'"> <span>Creating a route</span> <span>Creating a route</span> <button class="btn-commit-seg" @click="commitPath()">Complete creation</button ><!--Button to end the path--> <button @click="cancelPath()">Cancel</button> </template> </template> <template v-else-if="activeSegment.state === 'EDIT'"> <span>Modifying a path</span> <button class="btn-commit-seg" @click="commitPath()"> <button class="btn-commit-seg" @click="commitPath()"> edit complete </button> </template> </div> </div> </div> </template> <script> </script import startSegment, { segState } from "./segment"; export default { name: "App", components: {}, data() { return { mapInstance: null, // activeSegment: null, segments: [], // list of pathes }; }, computed: { activeSegment: () => segState.target, }, 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); // create map and return object } methods: { processNewPath() { console.log("[new path] start"); // pass map object startSegment(this.mapInstance); }, commitPath() { if (this.activeSegment.state === "NEW") { // only add if new this.segments.push(this.activeSegment); // register it first } }, this.activeSegment.commit(); // reference deleted } cancelPath() { this.activeSegment.dispose(); // for path removal }, /**] * @param seg segment to clear (path) * @param index index value */** @param index deletePath(seg, index) { this.segments.splice(index, 1); // only one seg.dispose(); }, }, }; </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%; display: flex; .path-list { width: 240px; } .list-of-seg { .segment { display: flex; padding-right: 8px; margin: 4px 0px; h4 { margin: 0; flex: 1 1 auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 4px 8px; } button { white-space: nowrap; padding: 4px 8px; border: 1px solid #ccc; background-color: white; border-radius: 4px; color: #777; &:hover { background-color: #efefef; color: #333; cursor: pointer; } &:active { background-color: #ddd; color: #000; } } } } .map-wrapper { flex: 1 1 auto; .map-control { position: absolute; top: 5px; left: 5px; z-index: 1000; } } } </style>
  • moving activeSegment from data() to computed. this imaginary variable returns the value of segState.target.
  • rendering different control buttons based on path creation and path modification states

2

later...