AmapTools

一款高德地图扩展工具。拦截高德地图(驾车、公交、步行)路线规划接口数据,将其转换成GeoJSON格式数据,并提供复制和下载。

  1. // ==UserScript==
  2. // @name AmapTools
  3. // @description 一款高德地图扩展工具。拦截高德地图(驾车、公交、步行)路线规划接口数据,将其转换成GeoJSON格式数据,并提供复制和下载。
  4. // @version 1.0.2
  5. // @author DD1024z
  6. // @namespace https://github.com/10D24D/AmapTools/
  7. // @supportURL https://github.com/10D24D/AmapTools/
  8. // @match https://www.amap.com/*
  9. // @match https://ditu.amap.com/*
  10. // @match https://www.gaode.com/*
  11. // @icon https://a.amap.com/pc/static/favicon.ico
  12. // @license MIT
  13. // @grant none
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. 'use strict';
  18.  
  19. let responseData = null; // 拦截到的接口数据
  20. let routeType = ''; // 当前路线类型(驾车、公交或步行)
  21. let listGeoJSON = []
  22. let currentGeoJSON = {}
  23. let selectedPathIndex = -1;
  24. let isDragging = false;
  25. let dragOffsetX = 0;
  26. let dragOffsetY = 0;
  27. let panelPosition = { left: null, top: null }; // 保存面板位置
  28.  
  29. const directionMap = {
  30. "driving": "驾车",
  31. "transit": "公交",
  32. "walking": "步行",
  33. }
  34. const uriMap = {
  35. "driving": "/service/autoNavigat",
  36. "transit": "/service/nav/bus",
  37. "walking": "/v3/direction/walking",
  38. }
  39.  
  40. // 样式封装
  41. const style = document.createElement('style');
  42. style.innerHTML = `
  43. #routeOptions {
  44. position: fixed;
  45. z-index: 9999;
  46. background-color: #f9f9f9;
  47. border: 1px solid #ccc;
  48. padding: 10px;
  49. box-shadow: 0 2px 2px rgba(0, 0, 0, .15);
  50. background: #fff;
  51. width: 300px;
  52. border-radius: 3px;
  53. font-family: Arial, sans-serif;
  54. cursor: move;
  55. }
  56. #routeOptions #closeBtn {
  57. position: absolute;
  58. top: -12px;
  59. right: 0px;
  60. background-color: transparent;
  61. color: #b3b3b3;
  62. border: none;
  63. font-size: 24px;
  64. cursor: pointer;
  65. }
  66. #routeOptions h3 {
  67. color: #333;
  68. font-size: 14px;
  69. }
  70. #routeOptions label {
  71. display: block;
  72. margin-bottom: 8px;
  73. }
  74. #routeOptions button {
  75. margin-top: 5px;
  76. padding: 5px 10px;
  77. cursor: pointer;
  78. }
  79. `;
  80. document.head.appendChild(style);
  81.  
  82. // 拦截 XMLHttpRequest 请求
  83. (function (open) {
  84. XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
  85. if (url.includes(uriMap.driving) || url.includes(uriMap.transit)) {
  86. this.addEventListener('load', function () {
  87. if (this.readyState === 4 && this.status === 200) {
  88. try {
  89. routeType = url.includes(uriMap.driving) ? directionMap.driving : directionMap.transit;
  90. responseData = JSON.parse(this.responseText);
  91. parseDataToGeoJSON();
  92. } catch (e) {
  93. responseData = null;
  94. console.error('解析路线数据时出错', e);
  95. }
  96. }
  97. });
  98.  
  99. }
  100. open.apply(this, arguments);
  101. };
  102. })(XMLHttpRequest.prototype.open);
  103.  
  104. // 拦截 script 请求
  105. const observer = new MutationObserver(function (mutations) {
  106. mutations.forEach(function (mutation) {
  107. mutation.addedNodes.forEach(function (node) {
  108. // 动态拦截步行路线的 JSONP 请求
  109. if (node.tagName === 'SCRIPT' && node.src.includes(uriMap.walking)) {
  110. const callbackName = /callback=([^&]+)/.exec(node.src)[1];
  111. if (callbackName && window[callbackName]) {
  112. const originalCallback = window[callbackName];
  113. window[callbackName] = function (data) {
  114. routeType = directionMap.walking;
  115. responseData = data;
  116. parseDataToGeoJSON();
  117. if (originalCallback) {
  118. originalCallback(data);
  119. }
  120. };
  121. }
  122. }
  123. });
  124. });
  125. });
  126.  
  127. observer.observe(document.body, { childList: true, subtree: true });
  128.  
  129. const lineGeoJSONTemplate = {
  130. type: "Feature",
  131. geometry: {
  132. type: "LineString",
  133. coordinates: []
  134. },
  135. properties: {}
  136. };
  137.  
  138. // 初始化一个路线的geojson
  139. function initLineGeoJSON() {
  140. return JSON.parse(JSON.stringify(lineGeoJSONTemplate)); // 深拷贝模板对象
  141. }
  142.  
  143. // 将原始数据转换成geojson
  144. function parseDataToGeoJSON() {
  145. listGeoJSON = [];
  146. let pathList = [];
  147.  
  148. if (routeType === directionMap.driving) {
  149. // 解析驾车规划的数据
  150. pathList = responseData.data.path_list;
  151. pathList.forEach((data, index) => {
  152. let geoJSON = initLineGeoJSON();
  153. geoJSON.properties.duration = Math.ceil(responseData.data.drivetime.split(',')[index] / 60)
  154. geoJSON.properties.distance = parseInt(responseData.data.distance.split(',')[index], 10)
  155. geoJSON.properties.traffic_lights = parseInt(data.traffic_lights || 0, 10)
  156.  
  157. data.path.forEach((path, index) => {
  158. path.segments.forEach((segment, index) => {
  159. if (segment.coor) {
  160. // 去掉 `[]` 符号
  161. const cleanedCoor = segment.coor.replace(/[\[\]]/g, '');
  162. const coorArray = cleanedCoor.split(',').map(Number);
  163. for (let k = 0; k < coorArray.length; k += 2) {
  164. const lng = coorArray[k];
  165. const lat = coorArray[k + 1];
  166. if (!isNaN(lng) && !isNaN(lat)) {
  167. geoJSON.geometry.coordinates.push([lng, lat]);
  168. }
  169. }
  170. }
  171. });
  172. });
  173.  
  174. listGeoJSON.push(geoJSON)
  175. });
  176. } else if (routeType === directionMap.transit) {
  177. // 解析公交规划的数据
  178. if (responseData.data.routelist && responseData.data.routelist.length > 0) {
  179. // 如果存在 routelist 则优先处理 routelist
  180. pathList = responseData.data.routelist;
  181.  
  182. // 处理 routelist 数据结构
  183. pathList.forEach((segment, index) => {
  184. let geoJSON = initLineGeoJSON();
  185. segment.segments.forEach((subSegment, i) => {
  186. subSegment.forEach((element, j) => {
  187. // 铁路。拼接起点、途经点和终点坐标
  188. if (element[0] === "railway") {
  189. // 添加起点坐标
  190. const startCoord = element[1].scord.split(' ').map(Number);
  191. geoJSON.geometry.coordinates.push(startCoord);
  192.  
  193. // 添加途经点坐标
  194. const viaCoords = element[1].viastcord.split(' ').map(Number);
  195. for (let k = 0; k < viaCoords.length; k += 2) {
  196. geoJSON.geometry.coordinates.push([viaCoords[k], viaCoords[k + 1]]);
  197. }
  198.  
  199. // 添加终点坐标
  200. const endCoord = element[1].tcord.split(' ').map(Number);
  201. geoJSON.geometry.coordinates.push(endCoord);
  202. }
  203. });
  204.  
  205. });
  206. geoJSON.properties.duration = parseInt(segment.time, 10); // 路程时间(单位:分钟)
  207. geoJSON.properties.distance = parseInt(segment.distance, 10); // 路程距离(单位:米)
  208. geoJSON.properties.cost = parseFloat(segment.cost); // 花费金额
  209. listGeoJSON.push(geoJSON);
  210. });
  211.  
  212. } else {
  213. // 过滤掉没有 busindex 的公交路线
  214. pathList = responseData.data.buslist.filter(route => route.busindex !== undefined);
  215.  
  216. pathList.forEach(data => {
  217. let geoJSON = initLineGeoJSON();
  218.  
  219. geoJSON.properties.distance = parseInt(data.allLength, 10)
  220. geoJSON.properties.duration = Math.ceil(data.expensetime / 60)
  221. geoJSON.properties.walk_distance = parseInt(data.allfootlength, 10)
  222. geoJSON.properties.expense = Math.ceil(data.expense)
  223. geoJSON.properties.expense_currency = data.expense_currency
  224.  
  225. const segmentList = data.segmentlist;
  226. let segmentProperties = []
  227.  
  228. segmentList.forEach(segment => {
  229. if (!geoJSON.properties.startStation) {
  230. geoJSON.properties.startStation = segment.startname + (geoJSON.properties.inport ? '(' + geoJSON.properties.inport + ')' : '');
  231. }
  232.  
  233. let importantInfo = {
  234. startname: segment.startname ? segment.startname : '',
  235. endname: segment.endname ? segment.endname : '',
  236. bus_key_name: segment.bus_key_name ? segment.bus_key_name : '',
  237. inport_name: segment.inport.name ? segment.inport.name : '',
  238. outport_name: segment.outport.name ? segment.outport.name : '',
  239. }
  240. segmentProperties.push(importantInfo);
  241.  
  242. // 起点到公交的步行路径
  243. if (segment.walk && segment.walk.infolist) {
  244. segment.walk.infolist.forEach(info => {
  245. const walkCoords = info.coord.split(',').map(Number);
  246. for (let i = 0; i < walkCoords.length; i += 2) {
  247. geoJSON.geometry.coordinates.push([walkCoords[i], walkCoords[i + 1]]);
  248. }
  249. });
  250. }
  251. // 公交驾驶路线
  252. const driverCoords = segment.drivercoord.split(',').map(Number);
  253. for (let i = 0; i < driverCoords.length; i += 2) {
  254. geoJSON.geometry.coordinates.push([driverCoords[i], driverCoords[i + 1]]);
  255. }
  256.  
  257. // 公交换乘路线
  258. // if (segment.alterlist && segment.alterlist.length > 0){
  259. // for (let i = 0; i < segment.alterlist.length; i++) {
  260. // const after = array[i];
  261.  
  262. // }
  263. // }
  264. });
  265.  
  266. // 到达公交后离终点的步行路径
  267. if (data.endwalk && data.endwalk.infolist) {
  268. data.endwalk.infolist.forEach(info => {
  269. const endwalkCoords = info.coord.split(',').map(Number);
  270. for (let i = 0; i < endwalkCoords.length; i += 2) {
  271. geoJSON.geometry.coordinates.push([endwalkCoords[i], endwalkCoords[i + 1]]);
  272. }
  273. });
  274. }
  275.  
  276. listGeoJSON.push(geoJSON);
  277. });
  278. }
  279.  
  280. } else if (routeType === directionMap.walking) {
  281. // 解析步行规划的数据
  282. pathList = responseData.route.paths;
  283. pathList.forEach(path => {
  284. let geoJSON = initLineGeoJSON()
  285. geoJSON.properties.distance = parseInt(path.distance, 10)
  286. geoJSON.properties.duration = Math.ceil(parseInt(path.duration, 10) / 60)
  287. path.steps.forEach(step => {
  288. const coorArray = step.polyline.split(';').map(item => item.split(',').map(Number));
  289. coorArray.forEach(coordinate => {
  290. if (coordinate.length === 2 && !isNaN(coordinate[0]) && !isNaN(coordinate[1])) {
  291. geoJSON.geometry.coordinates.push(coordinate);
  292. }
  293. });
  294. });
  295. listGeoJSON.push(geoJSON);
  296. });
  297.  
  298. } else {
  299. console.error('未知的数据')
  300. return;
  301. }
  302.  
  303. displayRouteOptions()
  304. }
  305.  
  306. // 创建路线选择界面
  307. function displayRouteOptions() {
  308. const existingDiv = document.getElementById('routeOptions');
  309. if (existingDiv) {
  310. existingDiv.remove();
  311. }
  312.  
  313. const routeDiv = document.createElement('div');
  314. routeDiv.id = 'routeOptions';
  315.  
  316. // 检查是否有保存的位置数据
  317. if (panelPosition.left && panelPosition.top) {
  318. routeDiv.style.left = `${panelPosition.left}px`;
  319. routeDiv.style.top = `${panelPosition.top}px`;
  320. } else {
  321. // 如果没有保存的位置数据,使用默认位置
  322. routeDiv.style.right = '20px';
  323. routeDiv.style.top = '100px';
  324. }
  325.  
  326. // 创建关闭按钮
  327. const closeBtn = document.createElement('button');
  328. closeBtn.id = 'closeBtn';
  329. closeBtn.innerText = '×';
  330. closeBtn.onclick = function () {
  331. routeDiv.remove();
  332. };
  333. routeDiv.appendChild(closeBtn);
  334.  
  335. // 出行方式
  336. const modeTitle = document.createElement('h3');
  337. modeTitle.innerText = '出行方式:';
  338. routeDiv.appendChild(modeTitle);
  339.  
  340. const modeSelectionDiv = document.createElement('div');
  341. modeSelectionDiv.style.display = 'flex';
  342. modeSelectionDiv.style.flexDirection = 'row';
  343. modeSelectionDiv.style.flexWrap = 'wrap';
  344.  
  345. const modes = [directionMap.driving, directionMap.transit, directionMap.walking];
  346. const modeIds = ['carTab', 'busTab', 'walkTab'];
  347.  
  348. modes.forEach((mode, modeIndex) => {
  349. const modeLabel = document.createElement('label');
  350. const modeRadio = document.createElement('input');
  351. modeLabel.style.marginRight = '5px';
  352. modeRadio.type = 'radio';
  353. modeRadio.name = 'modeSelection';
  354. modeRadio.value = mode;
  355. modeRadio.onchange = function () {
  356. const modeTab = document.getElementById(modeIds[modeIndex]);
  357. if (modeTab) {
  358. modeTab.click(); // 触发高德地图相应Tab的点击事件
  359. }
  360. };
  361. if (mode === routeType) {
  362. modeRadio.checked = true;
  363. }
  364.  
  365. modeLabel.appendChild(modeRadio);
  366. modeLabel.appendChild(document.createTextNode(mode));
  367. modeSelectionDiv.appendChild(modeLabel);
  368. });
  369.  
  370. // 将 modeSelectionDiv 添加到路线选择界面
  371. routeDiv.appendChild(modeSelectionDiv);
  372.  
  373. // 修改原来的标题
  374. const title = document.createElement('h3');
  375. title.innerText = `路线列表:`;
  376. routeDiv.appendChild(title);
  377. const routeFragment = document.createDocumentFragment();
  378.  
  379. // 遍历所有的路线
  380. listGeoJSON.forEach((geoJSON, index) => {
  381. const label = document.createElement('label');
  382. const radio = document.createElement('input');
  383. radio.type = 'radio';
  384. radio.name = 'routeSelection';
  385. radio.value = index;
  386.  
  387. radio.onclick = function () {
  388. selectedPathIndex = index;
  389. currentGeoJSON = listGeoJSON[selectedPathIndex]
  390. copyToClipboard(JSON.stringify(currentGeoJSON));
  391. // console.log("选中的路线:", currentGeoJSON);
  392.  
  393. // 同步点击高德地图的路线选项
  394. // 去除所有元素的 open 样式
  395. document.querySelectorAll('.planTitle.open').forEach(function (el) {
  396. el.classList.remove('open');
  397. });
  398. // 为当前选中的路线添加 open 样式
  399. const currentPlanTitle = document.getElementById(`plantitle_${index}`);
  400. if (currentPlanTitle) {
  401. currentPlanTitle.classList.add('open');
  402. currentPlanTitle.click();
  403. }
  404. };
  405.  
  406. if (index === 0) {
  407. radio.checked = true;
  408. selectedPathIndex = 0;
  409. currentGeoJSON = listGeoJSON[selectedPathIndex]
  410. copyToClipboard(JSON.stringify(currentGeoJSON));
  411. // console.log("选中的路线:", currentGeoJSON);
  412. }
  413.  
  414. const totalDistance = formatDistance(geoJSON.properties.distance);
  415.  
  416. const totalTime = formatTime(geoJSON.properties.duration);
  417.  
  418. const trafficLights = geoJSON.properties.traffic_lights ? ` | 红绿灯${geoJSON.properties.traffic_lights}个` : '';
  419.  
  420. const walkDistance = geoJSON.properties.walk_distance ? ` | 步行${formatDistance(geoJSON.properties.walk_distance)}` : '';
  421.  
  422. const expense = geoJSON.properties.expense ? ` | ${Math.ceil(geoJSON.properties.expense)}${geoJSON.properties.expense_currency}` : '';
  423.  
  424. label.appendChild(radio);
  425. label.appendChild(document.createTextNode(`路线${index + 1}:约${totalTime} | ${totalDistance}${trafficLights}${walkDistance}${expense}`));
  426. routeFragment.appendChild(label);
  427. });
  428. routeDiv.appendChild(routeFragment);
  429.  
  430. const downloadBtn = document.createElement('button');
  431. downloadBtn.innerText = '下载GeoJSON';
  432. downloadBtn.onclick = function () {
  433. if (selectedPathIndex === -1) {
  434. alert("请先选择一条路线");
  435. return;
  436. }
  437. currentGeoJSON = listGeoJSON[selectedPathIndex]
  438. downloadGeoJSON(currentGeoJSON, `${routeType}_路线${selectedPathIndex + 1}.geojson`);
  439. };
  440. routeDiv.appendChild(downloadBtn);
  441.  
  442. document.body.appendChild(routeDiv);
  443.  
  444. // 添加拖拽功能
  445. routeDiv.addEventListener('mousedown', function (e) {
  446. isDragging = true;
  447. dragOffsetX = e.clientX - routeDiv.offsetLeft;
  448. dragOffsetY = e.clientY - routeDiv.offsetTop;
  449. routeDiv.style.cursor = 'grabbing';
  450. });
  451.  
  452. document.addEventListener('mousemove', function (e) {
  453. if (isDragging) {
  454. const newLeft = Math.max(0, Math.min(window.innerWidth - routeDiv.offsetWidth, e.clientX - dragOffsetX));
  455. const newTop = Math.max(0, Math.min(window.innerHeight - routeDiv.offsetHeight, e.clientY - dragOffsetY));
  456. routeDiv.style.left = `${newLeft}px`;
  457. routeDiv.style.top = `${newTop}px`;
  458.  
  459. // 保存新的位置到 panelPosition
  460. panelPosition.top = newTop;
  461. panelPosition.left = newLeft;
  462. }
  463. });
  464.  
  465. document.addEventListener('mouseup', function () {
  466. isDragging = false;
  467. routeDiv.style.cursor = 'move';
  468. });
  469. }
  470.  
  471. // 时间格式化:大于60分钟显示小时,大于24小时显示天
  472. function formatTime(minutes) {
  473. if (minutes >= 1440) { // 超过24小时
  474. const days = Math.floor(minutes / 1440);
  475. const hours = Math.floor((minutes % 1440) / 60);
  476. return `${days}天${hours ? hours + '小时' : ''}`;
  477. } else if (minutes >= 60) { // 超过1小时
  478. const hours = Math.floor(minutes / 60);
  479. const mins = minutes % 60;
  480. return `${hours}小时${mins ? mins + '分钟' : ''}`;
  481. }
  482. return `${minutes}分钟`;
  483. }
  484.  
  485. // 格式化距离函数:如果小于1000米,保留米;如果大于等于1000米,转换为公里
  486. function formatDistance(distanceInMeters) {
  487. if (distanceInMeters < 1000) {
  488. return `${distanceInMeters}米`;
  489. } else {
  490. return `${(distanceInMeters / 1000).toFixed(1)}公里`;
  491. }
  492. }
  493.  
  494. // 复制内容到剪贴板
  495. function copyToClipboard(text) {
  496. if (navigator.clipboard && window.isSecureContext) {
  497. navigator.clipboard.writeText(text).then(() => {
  498. console.log("GeoJSON已复制到剪贴板");
  499. }).catch(() => fallbackCopyToClipboard(text));
  500. } else {
  501. fallbackCopyToClipboard(text);
  502. }
  503. }
  504.  
  505.  
  506. // 备用复制方案
  507. function fallbackCopyToClipboard(text) {
  508. const textarea = document.createElement('textarea');
  509. textarea.value = text;
  510. document.body.appendChild(textarea);
  511. textarea.select();
  512. try {
  513. document.execCommand('copy');
  514. console.log("GeoJSON已复制到剪贴板");
  515. } catch (err) {
  516. console.error("备用复制方案失败: ", err);
  517. }
  518. document.body.removeChild(textarea);
  519. }
  520.  
  521. // 下载GeoJSON文件
  522. function downloadGeoJSON(geoJSON, filename) {
  523. const blob = new Blob([JSON.stringify(geoJSON)], { type: 'application/json' });
  524. const link = document.createElement('a');
  525. link.href = URL.createObjectURL(blob);
  526. link.download = filename;
  527. document.body.appendChild(link);
  528. link.click();
  529. document.body.removeChild(link);
  530. }
  531.  
  532. // AmapLoginAssist - 高德地图支持密码登录、三方登录
  533. // [clone from MIT code](https://greatest.deepsurf.us/zh-CN/scripts/477376-amaploginassist-%E9%AB%98%E5%BE%B7%E5%9C%B0%E5%9B%BE%E6%94%AF%E6%8C%81%E5%AF%86%E7%A0%81%E7%99%BB%E5%BD%95-%E4%B8%89%E6%96%B9%E7%99%BB%E5%BD%95)
  534. let pollCount = 0;
  535. let intervalID = setInterval(() => {
  536. try {
  537. pollCount++;
  538. if (pollCount > 50) {
  539. clearInterval(intervalID);
  540. return;
  541. }
  542.  
  543. //
  544. if (window.passport && window.passport.config) {
  545. clearInterval(intervalID);
  546. window.passport.config({
  547. loginMode: ["password", "message", "qq", "sina", "taobao", "alipay", "subAccount", "qrcode"],
  548. loginParams: {
  549. dip: 20303
  550. }
  551. });
  552. window.passport.config = () => { };
  553. }
  554.  
  555. } catch (e) {
  556. console.error(e)
  557. clearInterval(intervalID);
  558. }
  559. }, 100);
  560. })();