Greasy Fork 支持简体中文。

AmapTools

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

// ==UserScript==
// @name         AmapTools
// @description  一款高德地图扩展工具。拦截高德地图(驾车、公交、步行)路线规划接口数据,将其转换成GeoJSON格式数据,并提供复制和下载。
// @version      1.0.2
// @author       DD1024z
// @namespace    https://github.com/10D24D/AmapTools/
// @supportURL   https://github.com/10D24D/AmapTools/
// @match        https://www.amap.com/*
// @match        https://ditu.amap.com/*
// @match        https://www.gaode.com/*
// @icon         https://a.amap.com/pc/static/favicon.ico
// @license      MIT
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    let responseData = null; // 拦截到的接口数据
    let routeType = ''; // 当前路线类型(驾车、公交或步行)
    let listGeoJSON = []
    let currentGeoJSON = {}
    let selectedPathIndex = -1;
    let isDragging = false;
    let dragOffsetX = 0;
    let dragOffsetY = 0;
    let panelPosition = { left: null, top: null }; // 保存面板位置

    const directionMap = {
        "driving": "驾车",
        "transit": "公交",
        "walking": "步行",
    }
    const uriMap = {
        "driving": "/service/autoNavigat",
        "transit": "/service/nav/bus",
        "walking": "/v3/direction/walking",
    }

    // 样式封装
    const style = document.createElement('style');
    style.innerHTML = `
        #routeOptions {
            position: fixed;
            z-index: 9999;
            background-color: #f9f9f9;
            border: 1px solid #ccc;
            padding: 10px;
            box-shadow: 0 2px 2px rgba(0, 0, 0, .15);
            background: #fff;
            width: 300px;
            border-radius: 3px;
            font-family: Arial, sans-serif;
            cursor: move;
        }
        #routeOptions #closeBtn {
            position: absolute;
            top: -12px;
            right: 0px;
            background-color: transparent;
            color: #b3b3b3;
            border: none;
            font-size: 24px;
            cursor: pointer;
        }
        #routeOptions h3 {
            color: #333;
            font-size: 14px;
        }
        #routeOptions label {
            display: block;
            margin-bottom: 8px;
        }
        #routeOptions button {
            margin-top: 5px;
            padding: 5px 10px;
            cursor: pointer;
        }
    `;
    document.head.appendChild(style);

    // 拦截 XMLHttpRequest 请求
    (function (open) {
        XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
            if (url.includes(uriMap.driving) || url.includes(uriMap.transit)) {
                this.addEventListener('load', function () {
                    if (this.readyState === 4 && this.status === 200) {
                        try {
                            routeType = url.includes(uriMap.driving) ? directionMap.driving : directionMap.transit;
                            responseData = JSON.parse(this.responseText);
                            parseDataToGeoJSON();
                        } catch (e) {
                            responseData = null;
                            console.error('解析路线数据时出错', e);
                        }
                    }
                });

            }
            open.apply(this, arguments);
        };
    })(XMLHttpRequest.prototype.open);

    // 拦截 script 请求
    const observer = new MutationObserver(function (mutations) {
        mutations.forEach(function (mutation) {
            mutation.addedNodes.forEach(function (node) {
                // 动态拦截步行路线的 JSONP 请求
                if (node.tagName === 'SCRIPT' && node.src.includes(uriMap.walking)) {
                    const callbackName = /callback=([^&]+)/.exec(node.src)[1];
                    if (callbackName && window[callbackName]) {
                        const originalCallback = window[callbackName];
                        window[callbackName] = function (data) {
                            routeType = directionMap.walking;
                            responseData = data;
                            parseDataToGeoJSON();
                            if (originalCallback) {
                                originalCallback(data);
                            }
                        };
                    }
                }
            });
        });
    });

    observer.observe(document.body, { childList: true, subtree: true });

    const lineGeoJSONTemplate = {
        type: "Feature",
        geometry: {
            type: "LineString",
            coordinates: []
        },
        properties: {}
    };

    // 初始化一个路线的geojson
    function initLineGeoJSON() {
        return JSON.parse(JSON.stringify(lineGeoJSONTemplate)); // 深拷贝模板对象
    }

    // 将原始数据转换成geojson
    function parseDataToGeoJSON() {
        listGeoJSON = [];
        let pathList = [];

        if (routeType === directionMap.driving) {
            // 解析驾车规划的数据
            pathList = responseData.data.path_list;
            pathList.forEach((data, index) => {
                let geoJSON = initLineGeoJSON();
                geoJSON.properties.duration = Math.ceil(responseData.data.drivetime.split(',')[index] / 60)
                geoJSON.properties.distance = parseInt(responseData.data.distance.split(',')[index], 10)
                geoJSON.properties.traffic_lights = parseInt(data.traffic_lights || 0, 10)

                data.path.forEach((path, index) => {
                    path.segments.forEach((segment, index) => {
                        if (segment.coor) {
                            // 去掉 `[]` 符号
                            const cleanedCoor = segment.coor.replace(/[\[\]]/g, '');
                            const coorArray = cleanedCoor.split(',').map(Number);
                            for (let k = 0; k < coorArray.length; k += 2) {
                                const lng = coorArray[k];
                                const lat = coorArray[k + 1];
                                if (!isNaN(lng) && !isNaN(lat)) {
                                    geoJSON.geometry.coordinates.push([lng, lat]);
                                }
                            }
                        }
                    });
                });

                listGeoJSON.push(geoJSON)
            });
        } else if (routeType === directionMap.transit) {
            // 解析公交规划的数据
            if (responseData.data.routelist && responseData.data.routelist.length > 0) {
                // 如果存在 routelist 则优先处理 routelist
                pathList = responseData.data.routelist;

                // 处理 routelist 数据结构
                pathList.forEach((segment, index) => {
                    let geoJSON = initLineGeoJSON();
                    segment.segments.forEach((subSegment, i) => {
                        subSegment.forEach((element, j) => {
                            // 铁路。拼接起点、途经点和终点坐标
                            if (element[0] === "railway") {
                                // 添加起点坐标
                                const startCoord = element[1].scord.split(' ').map(Number);
                                geoJSON.geometry.coordinates.push(startCoord);

                                // 添加途经点坐标
                                const viaCoords = element[1].viastcord.split(' ').map(Number);
                                for (let k = 0; k < viaCoords.length; k += 2) {
                                    geoJSON.geometry.coordinates.push([viaCoords[k], viaCoords[k + 1]]);
                                }

                                // 添加终点坐标
                                const endCoord = element[1].tcord.split(' ').map(Number);
                                geoJSON.geometry.coordinates.push(endCoord);
                            }
                        });

                    });
                    geoJSON.properties.duration = parseInt(segment.time, 10); // 路程时间(单位:分钟)
                    geoJSON.properties.distance = parseInt(segment.distance, 10); // 路程距离(单位:米)
                    geoJSON.properties.cost = parseFloat(segment.cost); // 花费金额
                    listGeoJSON.push(geoJSON);
                });

            } else {
                // 过滤掉没有 busindex 的公交路线
                pathList = responseData.data.buslist.filter(route => route.busindex !== undefined);

                pathList.forEach(data => {
                    let geoJSON = initLineGeoJSON();

                    geoJSON.properties.distance = parseInt(data.allLength, 10)
                    geoJSON.properties.duration = Math.ceil(data.expensetime / 60)
                    geoJSON.properties.walk_distance = parseInt(data.allfootlength, 10)
                    geoJSON.properties.expense = Math.ceil(data.expense)
                    geoJSON.properties.expense_currency = data.expense_currency

                    const segmentList = data.segmentlist;
                    let segmentProperties = []

                    segmentList.forEach(segment => {
                        if (!geoJSON.properties.startStation) {
                            geoJSON.properties.startStation = segment.startname + (geoJSON.properties.inport ? '(' + geoJSON.properties.inport + ')' : '');
                        }

                        let importantInfo = {
                            startname: segment.startname ? segment.startname : '',
                            endname: segment.endname ? segment.endname : '',
                            bus_key_name: segment.bus_key_name ? segment.bus_key_name : '',
                            inport_name: segment.inport.name ? segment.inport.name : '',
                            outport_name: segment.outport.name ? segment.outport.name : '',
                        }
                        segmentProperties.push(importantInfo);

                        // 起点到公交的步行路径
                        if (segment.walk && segment.walk.infolist) {
                            segment.walk.infolist.forEach(info => {
                                const walkCoords = info.coord.split(',').map(Number);
                                for (let i = 0; i < walkCoords.length; i += 2) {
                                    geoJSON.geometry.coordinates.push([walkCoords[i], walkCoords[i + 1]]);
                                }
                            });
                        }
                        // 公交驾驶路线
                        const driverCoords = segment.drivercoord.split(',').map(Number);
                        for (let i = 0; i < driverCoords.length; i += 2) {
                            geoJSON.geometry.coordinates.push([driverCoords[i], driverCoords[i + 1]]);
                        }

                        // 公交换乘路线
                        // if (segment.alterlist && segment.alterlist.length > 0){
                        //     for (let i = 0; i < segment.alterlist.length; i++) {
                        //         const after = array[i];

                        //     }
                        // }
                    });

                    // 到达公交后离终点的步行路径
                    if (data.endwalk && data.endwalk.infolist) {
                        data.endwalk.infolist.forEach(info => {
                            const endwalkCoords = info.coord.split(',').map(Number);
                            for (let i = 0; i < endwalkCoords.length; i += 2) {
                                geoJSON.geometry.coordinates.push([endwalkCoords[i], endwalkCoords[i + 1]]);
                            }
                        });
                    }

                    listGeoJSON.push(geoJSON);
                });
            }

        } else if (routeType === directionMap.walking) {
            // 解析步行规划的数据
            pathList = responseData.route.paths;
            pathList.forEach(path => {
                let geoJSON = initLineGeoJSON()
                geoJSON.properties.distance = parseInt(path.distance, 10)
                geoJSON.properties.duration = Math.ceil(parseInt(path.duration, 10) / 60)
                path.steps.forEach(step => {
                    const coorArray = step.polyline.split(';').map(item => item.split(',').map(Number));
                    coorArray.forEach(coordinate => {
                        if (coordinate.length === 2 && !isNaN(coordinate[0]) && !isNaN(coordinate[1])) {
                            geoJSON.geometry.coordinates.push(coordinate);
                        }
                    });
                });
                listGeoJSON.push(geoJSON);
            });

        } else {
            console.error('未知的数据')
            return;
        }

        displayRouteOptions()
    }

    // 创建路线选择界面
    function displayRouteOptions() {
        const existingDiv = document.getElementById('routeOptions');
        if (existingDiv) {
            existingDiv.remove();
        }

        const routeDiv = document.createElement('div');
        routeDiv.id = 'routeOptions';

        // 检查是否有保存的位置数据
        if (panelPosition.left && panelPosition.top) {
            routeDiv.style.left = `${panelPosition.left}px`;
            routeDiv.style.top = `${panelPosition.top}px`;
        } else {
            // 如果没有保存的位置数据,使用默认位置
            routeDiv.style.right = '20px';
            routeDiv.style.top = '100px';
        }

        // 创建关闭按钮
        const closeBtn = document.createElement('button');
        closeBtn.id = 'closeBtn';
        closeBtn.innerText = '×';
        closeBtn.onclick = function () {
            routeDiv.remove();
        };
        routeDiv.appendChild(closeBtn);

        // 出行方式
        const modeTitle = document.createElement('h3');
        modeTitle.innerText = '出行方式:';
        routeDiv.appendChild(modeTitle);

        const modeSelectionDiv = document.createElement('div');
        modeSelectionDiv.style.display = 'flex';
        modeSelectionDiv.style.flexDirection = 'row';
        modeSelectionDiv.style.flexWrap = 'wrap';

        const modes = [directionMap.driving, directionMap.transit, directionMap.walking];
        const modeIds = ['carTab', 'busTab', 'walkTab'];

        modes.forEach((mode, modeIndex) => {
            const modeLabel = document.createElement('label');
            const modeRadio = document.createElement('input');
            modeLabel.style.marginRight = '5px';
            modeRadio.type = 'radio';
            modeRadio.name = 'modeSelection';
            modeRadio.value = mode;
            modeRadio.onchange = function () {
                const modeTab = document.getElementById(modeIds[modeIndex]);
                if (modeTab) {
                    modeTab.click(); // 触发高德地图相应Tab的点击事件
                }
            };
            if (mode === routeType) {
                modeRadio.checked = true;
            }

            modeLabel.appendChild(modeRadio);
            modeLabel.appendChild(document.createTextNode(mode));
            modeSelectionDiv.appendChild(modeLabel);
        });

        // 将 modeSelectionDiv 添加到路线选择界面
        routeDiv.appendChild(modeSelectionDiv);

        // 修改原来的标题
        const title = document.createElement('h3');
        title.innerText = `路线列表:`;
        routeDiv.appendChild(title);
        const routeFragment = document.createDocumentFragment();

        // 遍历所有的路线
        listGeoJSON.forEach((geoJSON, index) => {
            const label = document.createElement('label');
            const radio = document.createElement('input');
            radio.type = 'radio';
            radio.name = 'routeSelection';
            radio.value = index;

            radio.onclick = function () {
                selectedPathIndex = index;
                currentGeoJSON = listGeoJSON[selectedPathIndex]
                copyToClipboard(JSON.stringify(currentGeoJSON));
                // console.log("选中的路线:", currentGeoJSON);

                // 同步点击高德地图的路线选项
                // 去除所有元素的 open 样式
                document.querySelectorAll('.planTitle.open').forEach(function (el) {
                    el.classList.remove('open');
                });
                // 为当前选中的路线添加 open 样式
                const currentPlanTitle = document.getElementById(`plantitle_${index}`);
                if (currentPlanTitle) {
                    currentPlanTitle.classList.add('open');
                    currentPlanTitle.click();
                }
            };

            if (index === 0) {
                radio.checked = true;
                selectedPathIndex = 0;
                currentGeoJSON = listGeoJSON[selectedPathIndex]
                copyToClipboard(JSON.stringify(currentGeoJSON));
                // console.log("选中的路线:", currentGeoJSON);
            }

            const totalDistance = formatDistance(geoJSON.properties.distance);

            const totalTime = formatTime(geoJSON.properties.duration);

            const trafficLights = geoJSON.properties.traffic_lights ? ` | 红绿灯${geoJSON.properties.traffic_lights}个` : '';

            const walkDistance = geoJSON.properties.walk_distance ? ` | 步行${formatDistance(geoJSON.properties.walk_distance)}` : '';

            const expense = geoJSON.properties.expense ? ` | ${Math.ceil(geoJSON.properties.expense)}${geoJSON.properties.expense_currency}` : '';

            label.appendChild(radio);
            label.appendChild(document.createTextNode(`路线${index + 1}:约${totalTime} | ${totalDistance}${trafficLights}${walkDistance}${expense}`));
            routeFragment.appendChild(label);
        });
        routeDiv.appendChild(routeFragment);

        const downloadBtn = document.createElement('button');
        downloadBtn.innerText = '下载GeoJSON';
        downloadBtn.onclick = function () {
            if (selectedPathIndex === -1) {
                alert("请先选择一条路线");
                return;
            }
            currentGeoJSON = listGeoJSON[selectedPathIndex]
            downloadGeoJSON(currentGeoJSON, `${routeType}_路线${selectedPathIndex + 1}.geojson`);
        };
        routeDiv.appendChild(downloadBtn);

        document.body.appendChild(routeDiv);

        // 添加拖拽功能
        routeDiv.addEventListener('mousedown', function (e) {
            isDragging = true;
            dragOffsetX = e.clientX - routeDiv.offsetLeft;
            dragOffsetY = e.clientY - routeDiv.offsetTop;
            routeDiv.style.cursor = 'grabbing';
        });

        document.addEventListener('mousemove', function (e) {
            if (isDragging) {
                const newLeft = Math.max(0, Math.min(window.innerWidth - routeDiv.offsetWidth, e.clientX - dragOffsetX));
                const newTop = Math.max(0, Math.min(window.innerHeight - routeDiv.offsetHeight, e.clientY - dragOffsetY));
                routeDiv.style.left = `${newLeft}px`;
                routeDiv.style.top = `${newTop}px`;

                // 保存新的位置到 panelPosition
                panelPosition.top = newTop;
                panelPosition.left = newLeft;
            }
        });

        document.addEventListener('mouseup', function () {
            isDragging = false;
            routeDiv.style.cursor = 'move';
        });
    }

    // 时间格式化:大于60分钟显示小时,大于24小时显示天
    function formatTime(minutes) {
        if (minutes >= 1440) { // 超过24小时
            const days = Math.floor(minutes / 1440);
            const hours = Math.floor((minutes % 1440) / 60);
            return `${days}天${hours ? hours + '小时' : ''}`;
        } else if (minutes >= 60) { // 超过1小时
            const hours = Math.floor(minutes / 60);
            const mins = minutes % 60;
            return `${hours}小时${mins ? mins + '分钟' : ''}`;
        }
        return `${minutes}分钟`;
    }

    // 格式化距离函数:如果小于1000米,保留米;如果大于等于1000米,转换为公里
    function formatDistance(distanceInMeters) {
        if (distanceInMeters < 1000) {
            return `${distanceInMeters}米`;
        } else {
            return `${(distanceInMeters / 1000).toFixed(1)}公里`;
        }
    }

    // 复制内容到剪贴板
    function copyToClipboard(text) {
        if (navigator.clipboard && window.isSecureContext) {
            navigator.clipboard.writeText(text).then(() => {
                console.log("GeoJSON已复制到剪贴板");
            }).catch(() => fallbackCopyToClipboard(text));
        } else {
            fallbackCopyToClipboard(text);
        }
    }


    // 备用复制方案
    function fallbackCopyToClipboard(text) {
        const textarea = document.createElement('textarea');
        textarea.value = text;
        document.body.appendChild(textarea);
        textarea.select();
        try {
            document.execCommand('copy');
            console.log("GeoJSON已复制到剪贴板");
        } catch (err) {
            console.error("备用复制方案失败: ", err);
        }
        document.body.removeChild(textarea);
    }

    // 下载GeoJSON文件
    function downloadGeoJSON(geoJSON, filename) {
        const blob = new Blob([JSON.stringify(geoJSON)], { type: 'application/json' });
        const link = document.createElement('a');
        link.href = URL.createObjectURL(blob);
        link.download = filename;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    }

    // AmapLoginAssist - 高德地图支持密码登录、三方登录
    // [clone from MIT code](https://greasyfork.org/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)
    let pollCount = 0;
    let intervalID = setInterval(() => {
        try {
            pollCount++;
            if (pollCount > 50) {
                clearInterval(intervalID);
                return;
            }

            //
            if (window.passport && window.passport.config) {
                clearInterval(intervalID);
                window.passport.config({
                    loginMode: ["password", "message", "qq", "sina", "taobao", "alipay", "subAccount", "qrcode"],
                    loginParams: {
                        dip: 20303
                    }
                });
                window.passport.config = () => { };
            }

        } catch (e) {
            console.error(e)
            clearInterval(intervalID);
        }
    }, 100);
})();