Instantly share code, notes, and snippets.
Last active
October 13, 2025 17:46
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save d3m3vilurr/2729f57c59d70b657d3448f03f2248bf to your computer and use it in GitHub Desktop.
Inject GPX link to Naver bike route map
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Inject GPX link to Naver MAP | |
| // @namespace https://map.naver.com | |
| // @updateURL https://gist.github.com/d3m3vilurr/2729f57c59d70b657d3448f03f2248bf/raw/inject_gpx_link_to_naver_map.user.js | |
| // @downloadURL https://gist.github.com/d3m3vilurr/2729f57c59d70b657d3448f03f2248bf/raw/inject_gpx_link_to_naver_map.user.js | |
| // @version 0.7 | |
| // @description Add GPX route link for bike | |
| // @author Sunguk Lee <[email protected]> | |
| // @match https://map.naver.com/* | |
| // @grant none | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // hack restore console log | |
| // var i = document.createElement('iframe'); | |
| // i.style.display = 'none'; | |
| // document.body.appendChild(i); | |
| // window.console = i.contentWindow.console; | |
| const XHR = XMLHttpRequest; | |
| function makeGPXXML(summary, routes, guides, waypoints) { | |
| const lines = [ | |
| '<?xml version="1.0" encoding="UTF-8"?>', | |
| '<gpx xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/1" xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.cluetrust.com/XML/GPXDATA/1/0 http://www.cluetrust.com/Schemas/gpxdata10.xsd" version="1.1" creator="http://ridewithgps.com/">', | |
| ' <metadata>', | |
| ` <name><![CDATA[${summary.name}]]></name>`, | |
| ` <time>${new Date().toISOString()}</time>`, | |
| ' </metadata>', | |
| ]; | |
| for (const wpt of waypoints) { | |
| lines.push(` <wpt lat="${wpt.lat}" lon="${wpt.lon}">`); | |
| lines.push(` <name><![CDATA[${wpt.name}]]></name>`); | |
| //lines.push(` <desc>${wpt.desc}</desc>`); | |
| lines.push(' </wpt>'); | |
| } | |
| /* | |
| lines.push(' <rte>'); | |
| for (const wpt of guides) { | |
| lines.push(` <rtept lat="${wpt.lat}" lon="${wpt.lon}">`); | |
| lines.push(` <name><![CDATA[${wpt.name}]]></name>`); | |
| lines.push(` <desc><![CDATA[${wpt.desc}]]></desc>`); | |
| lines.push(' </rtept>'); | |
| } | |
| lines.push(' </rte>'); | |
| */ | |
| lines.push(' <trk>'); | |
| lines.push(` <name><![CDATA[${summary.name}]]></name>`); | |
| lines.push(' <trkseg>'); | |
| for (const route of routes) { | |
| lines.push(` <trkpt lat="${route.lat}" lon="${route.lon}"></trkpt>`); | |
| } | |
| lines.push(' </trkseg>'); | |
| lines.push(' </trk>'); | |
| //for (const wpt of guides) { | |
| // lines.push(` <wpt lat="${wpt.lat}" lon="${wpt.lon}">`); | |
| // lines.push(` <name>${wpt.name}</name>`); | |
| // lines.push(` <desc>${wpt.desc}</desc>`); | |
| // lines.push(' </wpt>'); | |
| //} | |
| lines.push('</gpx>'); | |
| return lines.join('\n'); | |
| } | |
| const sleep = (ms) => { | |
| return new Promise((res, rej) => { | |
| setTimeout(() => { res(); }, ms); | |
| }); | |
| }; | |
| const parseGuide = (guide) => { | |
| //const [lon, lat] = guide.turn_point.split(',').map((v) => parseFloat(v)); | |
| const [lon, lat] = guide.eye.length == 2 ? guide.eye : guide.turn_point.split(',').map((v) => parseFloat(v)); | |
| const desc = guide.instructions.replace(/(<([^>]+)>)/gi, ""); | |
| return { | |
| name: guide.point || desc, | |
| desc: desc, | |
| lat, lon, | |
| }; | |
| }; | |
| async function handleToMakeGPXLink(e) { | |
| const xhr = e.target; | |
| if (xhr.status != 200) { | |
| return; | |
| } | |
| //console.log(xhr.responseURL); | |
| if (!xhr.responseURL.includes('directions/bike')) { | |
| return; | |
| } | |
| let json; | |
| try { | |
| json = JSON.parse(xhr.responseText); | |
| console.log(json); | |
| } catch (e) { | |
| return; | |
| } | |
| await sleep(1000) | |
| let items = document.getElementsByTagName('li'); | |
| let docItems = []; | |
| for (const li of items) { | |
| for (const cls of li.classList) { | |
| if (cls.startsWith('directionsSummaryItem')) { | |
| docItems.push(li); | |
| break; | |
| } | |
| } | |
| } | |
| //let docItems = document.getElementsByTagName('directions-summary-item-bike'); | |
| console.log(docItems); | |
| if (!docItems.length) { | |
| return; | |
| } | |
| await sleep(1000); | |
| for (const idx in json.routes) { | |
| const route = json.routes[idx]; | |
| //const docItem = docItems[idx].getElementsByClassName('summary_box')[0]; | |
| const docItem = docItems[idx].getElementsByClassName('btn_way_detail')[0]; | |
| console.log(docItem); | |
| console.log('from:', route.summary.start.address); | |
| console.log('to:', route.summary.end.address); | |
| console.log('distance:', route.summary.distance); | |
| const waypoints = []; | |
| const routes = []; | |
| const guides = []; | |
| //for (const waypoint of route.road_summary) { | |
| // const [lon, lat] = waypoint.location.split(',').map((v) => parseFloat(v)); | |
| // waypoints.append({ | |
| // name: waypoint.address, | |
| // lat, | |
| // lon, | |
| // }); | |
| //} | |
| for (const leg of route.legs) { | |
| let sectionStart = true; | |
| for (const step of leg.steps) { | |
| // if (step.guide.turn_point) { | |
| // const [lon,lat] = step.guide.turn_point.split(',').map((v) => parseFloat(v)); | |
| // routes.push({lat, lon}); | |
| // } | |
| const paths = step.path.split(' '); | |
| const guide = parseGuide(step.guide); | |
| guides.push(guide); | |
| if (sectionStart) { | |
| waypoints.push(guide); | |
| sectionStart = false; | |
| } | |
| let firstPath = true; | |
| for (const path of paths) { | |
| if (!path) { | |
| continue; | |
| } | |
| const [lon,lat] = path.split(',').map((v) => parseFloat(v)); | |
| //if (firstPath) { | |
| // if (lon !== guide.lon || lat !== guide.lat) { | |
| // routes.push(guide); | |
| // } | |
| // firstPath = false; | |
| //} | |
| routes.push({lat, lon}); | |
| } | |
| //guides.push(parseGuide(step.guide)); | |
| } | |
| } | |
| // last guide is last waypoint | |
| waypoints.push(guides[guides.length - 1]); | |
| // console.log('routes:', routes); | |
| // console.log('guides:', guides); | |
| route.summary.name = `${route.summary.start.address}_${route.summary.end.address}`; | |
| const xml = makeGPXXML(route.summary, routes, guides, waypoints); | |
| const elem = document.createElement('a'); | |
| elem.setAttribute('href', 'data:text/plain;charset=utf-8,'+encodeURIComponent(xml)); | |
| elem.setAttribute('download', `${route.summary.name}.gpx`); | |
| elem.innerHTML = '<img src="https://upload.wikimedia.org/wikipedia/commons/e/e3/Gpx_icon.png" width="18" height="18" />' | |
| // console.log(elem); | |
| const span = document.createElement('span'); | |
| span.classList.add('summary_subtitle'); | |
| span.style.margin = "0 0 0 6px"; | |
| span.appendChild(elem); | |
| docItem.appendChild(span); | |
| } | |
| } | |
| class NaverMap2GPXXMLHttpRequest extends XHR { | |
| send(body) { | |
| // inject listener to hooking the loaded data | |
| this.addEventListener('loadend', handleToMakeGPXLink); | |
| return super.send(body); | |
| } | |
| } | |
| window.XMLHttpRequest = NaverMap2GPXXMLHttpRequest; | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment