图寻复盘工具 PRO

增加复盘小地图,全面提升复盘效果

// ==UserScript==
// @name         图寻复盘工具 PRO
// @namespace    https://greasyfork.org/users/1179204
// @version      1.7.8
// @description  增加复盘小地图,全面提升复盘效果
// @match        *://tuxun.fun/replay-pano?gameId=*&round=*
// @icon         data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDggNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0iIzAwMDAwMCI+PGcgaWQ9IlNWR1JlcG9fYmdDYXJyaWVyIiBzdHJva2Utd2lkdGg9IjAiPjwvZz48ZyBpZD0iU1ZHUmVwb190cmFjZXJDYXJyaWVyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjwvZz48ZyBpZD0iU1ZHUmVwb19pY29uQ2FycmllciI+PHRpdGxlPjcwIEJhc2ljIGljb25zIGJ5IFhpY29ucy5jbzwvdGl0bGU+PHBhdGggZD0iTTI0LDEuMzJjLTkuOTIsMC0xOCw3LjgtMTgsMTcuMzhBMTYuODMsMTYuODMsMCwwLDAsOS41NywyOS4wOWwxMi44NCwxNi44YTIsMiwwLDAsMCwzLjE4LDBsMTIuODQtMTYuOEExNi44NCwxNi44NCwwLDAsMCw0MiwxOC43QzQyLDkuMTIsMzMuOTIsMS4zMiwyNCwxLjMyWiIgZmlsbD0iI2ZmOTQyNyI+PC9wYXRoPjxwYXRoIGQ9Ik0yNS4zNywxMi4xM2E3LDcsMCwxLDAsNS41LDUuNUE3LDcsMCwwLDAsMjUuMzcsMTIuMTNaIiBmaWxsPSIjZmZmZmZmIj48L3BhdGg+PC9nPjwvc3ZnPg==
// @author       KaKa
// @license      BSD
// @grant        GM_setClipboard
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @require      https://cdn.jsdelivr.net/npm/sweetalert2@11
// @require      https://unpkg.com/[email protected]/dist/leaflet.js
// @require      https://unpkg.com/gcoord/dist/gcoord.global.prod.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/suncalc.min.js
// @require      https://unpkg.com/leaflet.heat/dist/leaflet-heat.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/chartjs-plugin-annotation.min.js
// ==/UserScript==

(function() {
    'use strict';
    GM_addStyle(`

    @import url('https://unpkg.com/[email protected]/dist/leaflet.css');

    #panels {
        position: fixed;
        top: 100px;
        left: 10px;
        padding: 10px;
        border-radius: 20px !important;
        z-index: 1000;
        display: flex;
        flex-direction: column;
        width: 180px;
    }

    #panels button {
        cursor: pointer;
        width: 100% !important;
        font-weight: bold !important;
        border: 8px solid #000000 !important;
        text-align: left !important;
        padding-left: 8px !important;
        padding-right: 8px !important;
        backdrop-filter: blur(10px);
        margin-bottom: 5px;
        border-radius: 4px;
        background-color: #000000 !important;
        color: #A0A0A0 !important;
    }

    #timeline {
        cursor: pointer;
        width: 100%;
        font-weight: bold;
        font-size:14px;
        border: 8px solid #000000;
        text-align: left;
        padding-left: 4px;
        padding-right: 2px;
        backdrop-filter: blur(10px);
        margin-bottom: 5px;
        border-radius: 4px;
        background-color: #000000;
        color: #A0A0A0;
    }

    #replay {
        cursor: pointer;
        width: 100%;
        font-weight: bold;
        font-size:16px;
        border: 8px solid #000000;
        text-align: left;
        padding-left: 4px;
        padding-right: 2px;
        backdrop-filter: blur(10px);
        margin-bottom: 5px;
        border-radius: 4px;
        background-color: #000000;
        color: #A0A0A0;
    }

    .custom-marker {
    background-color: red;
    color: white;
    border-radius: 50%;
    width: 20px;
    height: 20px;
    text-align: center;
    line-height: 20px;
    }

    .leaflet-tooltip {
       background: rgba(255, 255, 255, 0.8);
       border: 0.5px solid #ccc;
       border-radius: 4px;
       font-size: 13px;
       color: black;
       font-weight: bold;
    }

    .ripple {
        position: absolute;
        border-radius: 50%;
        background: rgba(0, 0, 0, 0.3);
        pointer-events: none;
        transform: scale(0);
        animation: ripple-animation 1s linear;
    }

     @keyframes ripple-animation {
        to {
            transform: scale(4);
            opacity: 0;}
        }
`);

    L.Projection.BaiduMercator = L.Util.extend({}, L.Projection.Mercator, {
        R: 6378206,
        R_MINOR: 6356584.314245179,
        bounds: new L.Bounds([-20037725.11268234, -19994619.55417086], [20037725.11268234, 19994619.55417086])
    });

    L.CRS.Baidu = L.Util.extend({}, L.CRS.Earth, {
        code: 'EPSG:Baidu',
        projection: L.Projection.BaiduMercator,
        transformation: new L.Transformation(1, 0.5, -1, 0.5),
        scale: function (zoom) { return 1 / Math.pow(2, (18 - zoom)); },
        zoom: function (scale) { return 18 - Math.log(1 / scale) / Math.LN2; },
    });

    L.TileLayer.BaiDuTileLayer = L.TileLayer.extend({
        initialize: function (param, options) {
            var templateImgUrl = "//maponline{s}.bdimg.com/starpic/u=x={x};y={y};z={z};v=009;type=sate&qt=satepc&fm=46&app=webearth2&v=009";
            var templateUrl = "//maponline{s}.bdimg.com/tile/?x={x}&y={y}&z={z}&{p}";
            var streetViewUrl = "//mapsv1.bdimg.com/?qt=tile&styles=pl&x={x}&y={y}&z={z}";
            var myUrl;
            if (param === "img") {
                myUrl = templateImgUrl;
            } else if (param === "streetview") {
                myUrl = streetViewUrl;
            } else {
                myUrl = templateUrl;
            }
            options = L.extend({
                getUrlArgs: function (o) { return { x: o.x, y: (-1 - o.y), z: o.z }; },
                p: param, subdomains: "0123", minZoom: 3, maxZoom: 19, minNativeZoom: 3, maxNativeZoom:19
            }, options);
            L.TileLayer.prototype.initialize.call(this, myUrl, options);
        },

        getTileUrl: function (coords) {
            if (this.options.getUrlArgs) {
                return L.Util.template(this._url, L.extend({ s: this._getSubdomain(coords), r: L.Browser.retina ? '@2x' : '' }, this.options.getUrlArgs(coords), this.options));
            } else {
                return L.TileLayer.prototype.getTileUrl.call(this, coords);
            }
        },
        _setZoomTransform: function (level, center, zoom) {
            center =L.latLng(gcoord.transform([center.lng, center.lat], gcoord.WGS84, gcoord.BD09).reverse())
            L.TileLayer.prototype._setZoomTransform.call(this, level, center, zoom);
        },
        _getTiledPixelBounds: function (center) {
            center = L.latLng(gcoord.transform([center.lng, center.lat], gcoord.WGS84, gcoord.BD09).reverse())
            return L.TileLayer.prototype._getTiledPixelBounds.call(this, center);
        }
    });

    L.tileLayer.baiDuTileLayer = function (param, options) { return new L.TileLayer.BaiDuTileLayer(param, options); };

    L.Control.OpacityControl = L.Control.extend({
        options: {
            position: 'topright'
        },

        initialize: function (layer, options) {
            this.layer = layer;
            L.setOptions(this, options);
        },

        onAdd: function (map) {
            var container = L.DomUtil.create('div', 'leaflet-control-opacity');
            this.container=container
            container.style.backgroundColor='#fff'
            container.style.width='100px'
            container.style.height='28px'
            container.style.boxShadow='rgba(0, 0, 0, 0.3) 0px 1px 4px -1px'
            container.style.borderRadius='5px'
            container.innerHTML = `
                <input type="range" id="opacity-slider" min="0" max="100" value="0" step="10" style="margin:5px; width:90px">
            `;
            L.DomEvent.disableClickPropagation(container);
            L.DomEvent.disableScrollPropagation(container);
            L.DomEvent.on(container.querySelector('#opacity-slider'), 'input', function (e) {
                var opacity = e.target.value / 100;
                this._currentOpacity = opacity;
                this.layer.setOpacity(opacity)
            }.bind(this));

            return container;
        },
        setOpacity: function(value){
            if(this.container) this.container.style.opacity=`${value}`
        }
    });

    L.control.opacityControl = function(opts) {
        return new L.Control.OpacityControl(opts);
    };

    function getCustomIcon(color, url) {
        if (!url) url="https://i.chao-fan.com/f58b7f52d7c801ba0806e2125a776a44.png"
        return L.divIcon({
            className: 'custom-icon',
            html: `
            <div class="marker-background" style="height:100%;width:100%; background-image: url(&quot;https://s.chao-fan.com/tuxun/images/marker_background_${color}.png&quot;); background-size: 100%; background-repeat: no-repeat; overflow:hidden;">
                <img src="https://i.chao-fan.com/${url}?x-oss-process=image/resize,h_80/quality,q_100" style="position: absolute; top: 38%; left: 50%; width:28px; height:28px; transform: translate(-50%, -50%); border-radius: 100%" />
            </div>
        `,
            iconSize: [30, 42],
            iconAnchor: [15, 42],
            popupAnchor: [1, -34],
            shadowSize: [42, 42]
        });
    }

    const flagIcon = new L.divIcon({
        className: 'custom-icon',
        html: `
            <div class="marker-background" style="height:100%;width:100%; background-image: url(&quot;https://s.chao-fan.com/tuxun/images/marker_background_black.png&quot;); background-size: 100%; background-repeat: no-repeat;">
                <span role="img" aria-label="flag" class="anticon anticon-flag" style="position:absolute; font-size: 20px; left:24%; top:16%"><svg viewBox="64 64 896 896" focusable="false" data-icon="flag" width="1em" height="1em" fill="currentColor" aria-hidden="true" style="transform: rotate(-45deg);"><path d="M184 232h368v336H184z" fill="#404040"></path><path d="M624 632c0 4.4-3.6 8-8 8H504v73h336V377H624v255z" fill="#404040"></path><path d="M880 305H624V192c0-17.7-14.3-32-32-32H184v-40c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v784c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V640h248v113c0 17.7 14.3 32 32 32h416c17.7 0 32-14.3 32-32V337c0-17.7-14.3-32-32-32zM184 568V232h368v336H184zm656 145H504v-73h112c4.4 0 8-3.6 8-8V377h216v336z" fill="warning"></path></svg></span>
            </div>
        `,
        iconSize: [36, 44],
        iconAnchor: [18, 44],
        popupAnchor: [1, -34],
    });

    let guideMap, map, heatMapLayer
    let service,svType,currentCRS,streetViewPanorama,requestUser
    let marker, pins=[],pathCoords=[],paths=[],userIcons={}
    let startPoint, previousPin
    let isMapDisplay=true, isJump=false, isFine=false, isPlaying=false
    let replay_data={}
    let currentLink
    let globalPanoId,startPanoId
    let globalTimeInfo
    let globalAreaInfo
    let globalStreetInfo
    let globalLat,globalLng
    let globalTimestamp
    let globalHeading
    let guesses

    let api_key=JSON.parse(localStorage.getItem('api_key'));

    let address_source=JSON.parse(localStorage.getItem('address_source'));

    let playerName=JSON.parse(localStorage.getItem('playerName'))

    let currentRound=getRound().round

    let currentGameId=getRound().id

    if (!address_source) {
        Swal.fire({
            title: '请选择获取地址信息的来源',
            icon: 'question',
            backdrop: null,
            text: 'OSM具有更详细的地址信息,高德地图的获取速度更快且带有电话区号信息(需要自行注册API密钥)',
            showCancelButton: true,
            allowOutsideClick: false,
            confirmButtonColor: '#3085d6',
            confirmButtonText: 'OSM',
            cancelButtonText: '高德地图',
        }).then((result) => {
            if (result.isConfirmed) {
                localStorage.setItem('address_source', JSON.stringify('OSM'));
                address_source='OSM'
            }
            else if (result.dismiss === Swal.DismissReason.cancel) {
                localStorage.setItem('address_source', JSON.stringify('GD'));
                address_source=JSON.parse(localStorage.getItem('address_source'))
                Swal.fire({
                    title: '请输入您的高德地图 API 密钥',
                    input: 'text',
                    inputPlaceholder: '',
                    showCancelButton: true,
                    backdrop: null,
                    confirmButtonText: '保存',
                    cancelButtonText: '取消',
                    preConfirm: (inputValue) => {
                        if (inputValue.length===32){
                            return inputValue;
                        }
                        else{
                            Swal.showValidationMessage('请输入有效的高德地图API密钥!')
                        }
                    }
                }).then((result) => {
                    if (result.isConfirmed) {
                        if(result.value){
                            localStorage.setItem('api_key', JSON.stringify(result.value));
                            Swal.fire('保存成功!', '您的API密钥已保存,请刷新页面。', 'success');}
                        else{
                            localStorage.removeItem('address_source')
                        }
                    }
                });

            }
        });
    }

    if(!api_key&&address_source==='GD'){
        Swal.fire({
            title: '请输入您的高德地图 API 密钥',
            input: 'text',
            inputPlaceholder: '',
            backdrop: null,
            showCancelButton: true,
            confirmButtonText: '保存',
            cancelButtonText: '取消',
            preConfirm: (inputValue) => {
                if (inputValue.length===32){
                    return inputValue;
                }
                else{
                    Swal.showValidationMessage('请输入有效的高德地图API密钥!')
                }
            }
        }).then((result) => {
            if (result.isConfirmed) {
                if(result.value){
                    api_key=JSON.parse(localStorage.getItem('api_key'));
                    Swal.fire('保存成功!', '您的API密钥已保存,请刷新页面。', 'success');}
            }
            else{
                localStorage.removeItem('address_source')
            }
        });
    }

    const container = document.createElement('div');
    container.id = 'panels';
    document.body.appendChild(container);

    const openButton = document.createElement('button');
    openButton.textContent = '在地图中打开';
    container.appendChild(openButton);

    const copyButton = document.createElement('button');
    copyButton.textContent = '复制街景链接';
    container.appendChild(copyButton);

    const mapButton = document.createElement('button');
    mapButton.textContent = '关闭小地图';
    container.appendChild(mapButton);


    openButton.onclick = () => {
        if(globalPanoId&&streetViewPanorama&&svType==='google'){
            const POV=streetViewPanorama.getPov()
            const zoom=streetViewPanorama.getZoom()
            const fov =calculateFOV(zoom)
            currentLink=`https://www.google.com/maps/@?api=1&map_action=pano&heading=${POV.heading}&pitch=${POV.pitch}&fov=${fov}&pano=${globalPanoId}`
        }
        window.open(currentLink, '_blank');
    }

    copyButton.onclick =async () => {
        const shortLink=await genShortLink()
        GM_setClipboard(shortLink, 'text');
        copyButton.textContent='复制成功!'
        setTimeout(function() {
            copyButton.textContent='复制街景链接'
        }, 1000)
    };

    mapButton.onclick = () => {
        if (isMapDisplay){
            guideMap.style.display='none'
            mapButton.textContent='显示小地图'
            isMapDisplay=false
        }
        else{
            guideMap.style.display='block'
            mapButton.textContent='关闭小地图'
            isMapDisplay=true
        }

    };

    const areaButton = document.createElement('button');
    areaButton.textContent = '地区';
    container.appendChild(areaButton);

    const streetButton = document.createElement('button');
    streetButton.textContent = '路名';
    container.appendChild(streetButton);

    const altitudeButton = document.createElement('button');
    altitudeButton.textContent = '海拔';
    container.appendChild(altitudeButton);

    /*const speedButton = document.createElement('button');
    speedButton.textContent = '车速';
    container.appendChild(speedButton);*/

    const downloadButton=document.createElement('button')
    downloadButton.textContent = '下载全景';
    container.appendChild(downloadButton);

    downloadButton.onclick =async () =>{
        const { value: zoom, dismiss: inputDismiss } = await Swal.fire({
            title: '请选择下载的图像质量等级\n(腾讯和百度无法选择)',
            html:'<select id="zoom-select" class="swal2-input" style="width:180px; height:40px; font-size:16px;white-space:prewrap">' +
            '<option value="1">高糊 (100KB~500KB)</option>' +
            '<option value="2">模糊 (500KB~1MB)</option>' +
            '<option value="3">标准 (1MB~4MB)</option>' +
            '<option value="4">高清 (4MB~8MB)</option>' +
            '<option value="5">原画 (8MB~15MB)</option>' +
            '</select>',
            icon: 'question',
            showCancelButton: true,
            showCloseButton: true,
            allowOutsideClick: false,
            confirmButtonColor: '#3085d6',
            cancelButtonColor: '#d33',
            confirmButtonText: 'Yes',
            cancelButtonText: 'Cancel',
            backdrop: null,
            preConfirm: () => {
                return document.getElementById('zoom-select').value;
            }
        });
        if (zoom){
            const fileName = `${globalPanoId}.jpg`;
            if(svType=='google'){
                const metaData = await searchGooglePano('GetMetadata', globalPanoId);

                var w=metaData.worldWidth
                var h=metaData.worldHeight
                }
            const swal = Swal.fire({
                title: '下载中',
                text: '请稍候',
                allowOutsideClick: false,
                allowEscapeKey: false,
                showConfirmButton: false,
                backdrop: null,
                didOpen: () => {
                    Swal.showLoading();
                }
            });
            await downloadPanoramaImage(globalPanoId, fileName,w,h,parseInt(zoom));
            swal.close()
            Swal.fire({
                title: '下载完成!',
                text: '全景图片已保存到你的电脑',
                icon: 'success',
                backdrop: false
            });
        }
    }

    const timeline = document.createElement('select');
    timeline.id='timeline'
    container.appendChild(timeline);
    timeline.addEventListener('change', function() {
        if(!streetViewPanorama)getSvContainer()
        streetViewPanorama.setPano(timeline.value);
    });

    const panoIdButton = document.createElement('button');
    panoIdButton.textContent = '全景Id';
    container.appendChild(panoIdButton);
    panoIdButton.onclick =async () => {
        if(!streetViewPanorama)getSvContainer()
        globalPanoId=streetViewPanorama.pano
        GM_setClipboard(globalPanoId, 'text');
        panoIdButton.textContent='复制成功!'
        setTimeout(function() {
            panoIdButton.textContent=globalPanoId&&svType=='baidu' ? `${globalPanoId.substring(6,10)}, ${globalPanoId.substring(25,27)}` : 'panoId'
        }, 1000)
    };

    const replayButton = document.createElement('button');
    replayButton.id='replay'
    container.appendChild(replayButton);
    replayButton.textContent = '查看回放';

    const chartButton = document.createElement('button');
    chartButton.id='replay'
    container.appendChild(chartButton);
    chartButton.textContent = '分析回放';
    let isHeatmapVisible = false;
    chartButton.onclick = () => {
        const isEmpty = Object.values(replay_data).every(value => value.length===0)
        if(isEmpty) return
        Swal.fire({
            title: '事件分析',
            html: `
        <div>
            <button style= "font-size: 18px;" id="togglePlayerBtn">切换玩家</button>
            <button id="toggleHeatMapBtn">地图分析</button>
            <button id="toggleSVBtn">街景分析</button>
            <canvas style="margin-top:10px; padding:5px" id="chartCanvas"></canvas>
            <div style="margin-top: 20px; display: flex; flex-wrap: wrap; gap: 20px;">
                <div style="flex: 1 1 45%; font-size: 14px;">
                    <p><strong>事件密度:</strong> <span id="eventDensity">加载中...</span></p>
                    <p><strong>次均停滞时间:</strong> <span id="switchCount">加载中...</span></p>
                    <p><strong>街景事件比例:</strong> <span id="streetViewRatio">加载中...</span></p>
                    <p><strong>第一次放大街景:</strong> <span id="firstPanoZoomTime">加载中...</span></p>
                    <p><strong>单次最长停滞时间:</strong> <span id="longestGapTime">加载中...</span></p>
                </div>
                <div style="flex: 1 1 45%; font-size: 14px;">
                    <p><strong>总停滞时间:</strong> <span id="stagnationTime">加载中...</span></p>
                    <p><strong>停滞次数:</strong> <span id="stagnationCount">加载中...</span></p>
                    <p><strong>地图事件比例:</strong> <span id="mapEventRatio">加载中...</span></p>
                    <p><strong>第一次放大地图:</strong> <span id="firstMapZoomTime">加载中...</span></p>
                    <p><strong>街景视角转动速度:</strong> <span id="avgPovSpeed">加载中...</span></p>
                </div>
            </div>
        </div>
    `,
            width: 800,
            showCloseButton: true,
            backdrop:null,
            didOpen: () => {
                const canvas = document.getElementById('chartCanvas')
                const ctx = canvas.getContext('2d', {willReadFrequently: true })
                const togglePlayerBtn = document.getElementById('togglePlayerBtn');
                const toggleHeatMapBtn = document.getElementById('toggleHeatMapBtn');
                const toggleSVBtn = document.getElementById('toggleSVBtn');

                const players = Object.keys(replay_data);
                let currentPlayerIndex = 0;
                togglePlayerBtn.textContent = `${players[currentPlayerIndex]}`

                function updateChartData(data, playerName) {
                    const interval = 1000;
                    const eventTypeMapping = {
                        "PanoPov": "街景视角",
                        "PanoZoom": "街景缩放",
                        "MapView": "地图视点",
                        "MapZoom": "地图缩放",
                        "Pin": "地图点击",
                        "MapStyle": "地图大小",
                        //"Switch": "切屏",
                        "MobileMap": "手机地图",
                        "PanoLocation": "街景移动"
                    };

                    const eventTypes = Object.values(eventTypeMapping);
                    const keyEventTypes = ["地图点击", "地图大小", "手机地图", "街景移动"];
                    const eventColors = {
                        "地图缩放": "#0000FF",
                        "地图视点": "#FFA500",
                        "街景视角": "#00FF00",
                        "街景缩放": "#FF0000",
                        "地图点击": "#00FFFF",
                        "地图大小": "#800080",
                        "手机地图": "#FFD700",
                        "街景移动": "#1E90FF"
                    };

                    const eventBuckets = {};
                    const allEventTimes = {};

                    eventTypes.forEach(eventType => {
                        eventBuckets[eventType] = {};

                    });
                    keyEventTypes.forEach(eventType => {
                        allEventTimes[eventType] = [];
                    });

                    data.forEach(event => {
                        const eventTime = event.time;
                        const relativeTime = eventTime - data[0].time;
                        const bucket = Math.floor(relativeTime / interval);

                        const translatedEventType = eventTypeMapping[event.action] || event.action;

                        if (eventTypes.includes(translatedEventType)) {
                            if (!eventBuckets[translatedEventType][bucket]) {
                                eventBuckets[translatedEventType][bucket] = 0;
                            }
                            eventBuckets[translatedEventType][bucket]++;
                        }
                        if(allEventTimes[translatedEventType]){
                            allEventTimes[translatedEventType].push(relativeTime); }
                    });

                    // 准备X轴标签(相对时间)
                    const labels = [];
                    const maxBucket = Math.max(
                        ...Object.values(eventBuckets).flatMap(bucket => Object.keys(bucket).map(Number))
                    );

                    for (let i = 0; i <= maxBucket; i++) {
                        const relativeSeconds = (i * interval + interval / 2) / 1000; // 获取3秒区间的中点
                        const minutes = Math.floor(relativeSeconds / 60);
                        const seconds = Math.floor(relativeSeconds % 60);
                        const formattedTime = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
                        labels.push(formattedTime);
                    }

                    const datasets = eventTypes.map(eventType => {
                        const dataPoints = labels.map((label, index) => eventBuckets[eventType][index] || 0);
                        return {
                            label: eventType,
                            data: dataPoints,
                            fill: false,
                            borderColor: eventColors[eventType],
                            backgroundColor: eventColors[eventType],
                            tension: 0.5,
                            hidden: true
                        };
                    });

                    const totalEventsData = labels.map((label, index) => {
                        let total = 0;
                        eventTypes.forEach(eventType => {
                            total += eventBuckets[eventType][index] || 0;
                        });
                        return total;
                    });

                    datasets.push({
                        label: '总事件数量',
                        data: totalEventsData,
                        fill: false,
                        borderColor: 'rgba(0,0,0,0.6)',
                        backgroundColor: 'rgba(0,0,0,0.6)',
                        tension: 0.5
                    });

                    const annotations = [];

                    Object.keys(allEventTimes).forEach(eventType => {
                        allEventTimes[eventType].forEach(eventTime => {
                            const xPosition = eventTime / 1000;
                            annotations.push({
                                type: 'line',
                                xMin: xPosition,
                                xMax: xPosition,
                                borderColor: eventColors[eventType],
                                borderWidth: 1.5,
                                borderDash: [5, 5],
                            });
                        });
                    });

                    chart.data.datasets = datasets;
                    chart.data.labels = labels;
                    chart.options.plugins.annotation.annotations = annotations; // 设置虚线标注
                    chart.update();}

                function toggleHeatMap() {
                    const data=replay_data[players[currentPlayerIndex]]
                    if(!heatMapLayer){
                        heatMapLayer = L.heatLayer([], {radius: 10,
                                                        blur: 5,
                                                        maxIntensity: 1}).addTo(map);}

                    const heatData = data.filter(event => ["MapZoom","MapView","Pin"].includes(event.action)).map(event => {
                        const coords = JSON.parse(event.data);
                        return [coords[0], coords[1], 200];
                    });

                    heatMapLayer.setLatLngs(heatData);
                }

                const chart = new Chart(ctx, {
                    type: 'line',
                    data: {
                        labels: [],
                        datasets: []
                    },
                    options: {
                        responsive: true,
                        plugins: {
                            legend: {
                                display: true,
                                labels: {
                                    boxWidth: 30,
                                    boxHeight: 15,
                                    padding: 30
                                },
                                position: 'top',
                                align: 'center',
                                labels: {
                                    usePointStyle: true,
                                    padding: 20,
                                    pointStyle: 'rectRounded'
                                },
                            },
                            tooltip: { mode: 'index', intersect: false,enabled:false },
                            annotation: {
                                annotations: [],
                            },
                        },
                        scales: {
                            x: { title: { display: true } },
                            y: { title: { display: true, text: '事件数量' }, beginAtZero: true }
                        },

                    },

                });

                function updateEventAnalysisData(data) {
                    const { eventDensity, stagnationTime, stagnationCount, switchCount, streetViewRatio, mapEventRatio, firstMapZoomTime, firstPanoZoomTime,longestGapTime,avgPovSpeed} = updateEventAnalysis(data);
                    document.getElementById('eventDensity').textContent = eventDensity.toFixed(2)+ " 次/秒";
                    document.getElementById('stagnationTime').textContent = stagnationTime.toFixed(2) + " 秒";
                    document.getElementById('longestGapTime').textContent = longestGapTime.toFixed(2) + " 秒";
                    document.getElementById('avgPovSpeed').textContent = avgPovSpeed.toFixed(2) + " 度/秒";
                    document.getElementById('stagnationCount').textContent = stagnationCount;
                    document.getElementById('switchCount').textContent = (parseFloat(stagnationTime/stagnationCount)).toFixed(2)+"秒";
                    document.getElementById('streetViewRatio').textContent = (streetViewRatio * 100).toFixed(2) + "%";
                    document.getElementById('mapEventRatio').textContent = (mapEventRatio * 100).toFixed(2) + "%";
                    document.getElementById('firstMapZoomTime').textContent = firstMapZoomTime === null ? "无" : '第'+firstMapZoomTime + "秒";
                    document.getElementById('firstPanoZoomTime').textContent = firstPanoZoomTime === null ? "无" : '第'+firstPanoZoomTime + "秒";
                }

                updateChartData(replay_data[players[currentPlayerIndex]], players[currentPlayerIndex]);
                updateEventAnalysisData(replay_data[players[currentPlayerIndex]]);

                togglePlayerBtn.onclick = () => {
                    currentPlayerIndex = (currentPlayerIndex + 1) % players.length;
                    togglePlayerBtn.textContent = `${players[currentPlayerIndex]}`;
                    updateChartData(replay_data[players[currentPlayerIndex]]);
                    toggleHeatMap()
                    updateEventAnalysisData(replay_data[players[currentPlayerIndex]]);
                };

                toggleHeatMapBtn.addEventListener('click', toggleHeatMap);

                toggleSVBtn.addEventListener('click',async () => {
                    var centerHeading;
                    if (svType == 'google') {
                        const metaData = await searchGooglePano('GetMetadata', globalPanoId);
                        var w = metaData.worldWidth;
                        var h = metaData.worldHeight;

                        centerHeading = metaData.heading;
                    }
                    else centerHeading=globalHeading
                    try {
                        const imageUrl = await downloadPanoramaImage(globalPanoId, globalPanoId, w, h, w==13312?5:3, true);
                        const img = await loadImage(imageUrl);
                        canvas.width = img.width;
                        canvas.height = img.height;
                        ctx.drawImage(img, 0, 0);

                        let lastPanoPov = { heading: 0, pitch: 0 };
                        let maxTimeDiff = 0;
                        let maxTimeDiffIndex = -1;
                        const heatData = replay_data[players[currentPlayerIndex]].filter(event => ["PanoZoom", "PanoPov"].includes(event.action)).map((event, index, events) => {
                            let heading, pitch, type;
                            let time = event.time;

                            if (event.action === "PanoPov") {
                                [heading, pitch] = JSON.parse(event.data);
                                lastPanoPov = { heading, pitch };
                                type = "PanoPov";
                            } else if (event.action === "PanoZoom") {
                                heading = lastPanoPov.heading;
                                pitch = lastPanoPov.pitch;
                                type = "PanoZoom";
                            }

                            if (index > 0) {
                                const prevEvent = events[index - 1];
                                const timeDiff = Math.abs(time - prevEvent.time);
                                if (timeDiff > maxTimeDiff) {
                                    maxTimeDiff = timeDiff;
                                    maxTimeDiffIndex = index;
                                }
                            }

                            return { heading, pitch, type, time};
                        });

                        drawHeatMapOnImage(canvas, heatData, centerHeading,maxTimeDiffIndex);
                    } catch (error) {
                        console.error('Error downloading panorama image:', error);
                    }

                })},
            willClose: () => {
                if (heatMapLayer) {
                    map.removeLayer(heatMapLayer);
                    heatMapLayer = null;
                }
            }
        });
    };

    replayButton.onclick = () => {
        const isEmpty = Object.values(replay_data).every(value => value.length===0)
        if(!isEmpty){
            Object.keys(replay_data).forEach((key) => {
                if(replay_data[key].length!=0){
                    const option = document.createElement('button');
                    option.value = key;
                    option.textContent = key;
                    option.addEventListener('click', function() {
                        const selectedKey = option.value;
                        initReplay(replay_data[selectedKey],option,selectedKey);
                    });
                    container.appendChild(option);
                }
            });
            container.removeChild(replayButton)}
        else replayButton.textContent = '无可用回放'
    };

    function drawHeatMapOnImage(canvas, heatData, centerHeading,maxTimeDiffIndex) {
        const ctx = canvas.getContext('2d');
        heatData.forEach((point, index) => {
            let headingDifference = point.heading - centerHeading;
            if (headingDifference > 180) {
                headingDifference -= 360;
            } else if (headingDifference < -180) {
                headingDifference += 360;
            }
            const x = (headingDifference + 180) / 360 * canvas.width;
            const y = (90 - point.pitch) / 180 * canvas.height;

            ctx.beginPath();
            ctx.arc(x, y, (index === maxTimeDiffIndex || index === maxTimeDiffIndex - 1)?40:20, 0,2* Math.PI);

            if (index === maxTimeDiffIndex || index === maxTimeDiffIndex - 1) {
                ctx.fillStyle = 'yellow';
            } else if (point.type === "PanoZoom") {
                ctx.fillStyle = '#FF0000';
            } else if (point.type === "PanoPov") {
                ctx.fillStyle = '#00FF00';
            }

            ctx.fill();
        });
    }
    function updateEventAnalysis(data) {
        let totalEvents = 0;
        let totalTime = 0;
        let stagnationTime = 0;
        let stagnationCount = 0;
        let switchCount = 0;
        let streetViewEvents = 0;
        let mapEvents = 0;
        let lastEventTime = null;
        let longestGapTime = 0;

        let totalHeadingDifference = 0;
        let totalTimeGap = 0;

        let lastPanoPovEventTime = null;
        let lastHeading = null;

        data.forEach(event => {
            const eventTime = event.time;
            const relativeTime = Math.floor((eventTime - data[0].time) / 1000);

            totalEvents++;
            totalTime = relativeTime;

            if (event.action.includes("Pano")) {
                streetViewEvents++;
            } else if (event.action.includes("Map")) {
                mapEvents++;
            }

            if (lastEventTime !== null) {
                const timeGap = (eventTime - lastEventTime) / 1000;

                if (timeGap >= 3) {
                    if (timeGap > longestGapTime) longestGapTime = timeGap;
                    stagnationTime += timeGap;
                    stagnationCount++;
                }
            }

            if (event.action === "PanoPov" && lastPanoPovEventTime !== null) {
                const headingDifference = Math.abs(JSON.parse(event.data)[0] - lastHeading);
                const timeGap = (eventTime - lastPanoPovEventTime) / 1000;

                totalHeadingDifference += headingDifference;
                totalTimeGap += timeGap;
            }

            lastEventTime = eventTime;

            if (event.action === "PanoPov") {
                lastPanoPovEventTime = eventTime;
                lastHeading = JSON.parse(event.data)[0];
            }

            if (event.action === "Switch" && event.data === "in") {
                switchCount++;
            }
        });

        const eventDensity = totalEvents / totalTime;

        const streetViewRatio = streetViewEvents / totalEvents;
        const mapEventRatio = mapEvents / totalEvents;

        let firstMapZoomTime = null;
        let firstPanoZoomTime_ = null;
        let firstPanoZoomTime = null;
        data.forEach(event => {
            if (event.action === "MapZoom" && firstMapZoomTime === null) {
                firstMapZoomTime = Math.floor((event.time - data[0].time) / 1000);
            }
            if (event.action === "PanoZoom" && !firstPanoZoomTime) {
                if (firstPanoZoomTime_ === null) firstPanoZoomTime_ = 1;
                else {
                    firstPanoZoomTime = Math.floor((event.time - data[0].time) / 1000);
                }
            }
        });

        let avgPovSpeed = 0;
        if (totalTimeGap > 0) {
            avgPovSpeed = totalHeadingDifference / totalTimeGap;
        }

        return {
            eventDensity,
            stagnationTime,
            stagnationCount,
            streetViewRatio,
            mapEventRatio,
            firstPanoZoomTime,
            firstMapZoomTime,
            longestGapTime,
            switchCount,
            avgPovSpeed
        };
    }


    async function genShortLink(){
        if(!streetViewPanorama)getSvContainer()

        if(globalPanoId){
            const location=streetViewPanorama.getPosition()
            const POV=streetViewPanorama.getPov()
            const zoom=streetViewPanorama.getZoom()
            var shortUrl
            if(svType==='google') shortUrl=await getGoogleSL(globalPanoId,location,POV.heading,POV.pitch,zoom);
            else if (svType==='qq') shortUrl=currentLink  //await getQQSL(globalPanoId,POV.heading,POV.pitch,zoom)
            else shortUrl=await getBDSL(globalPanoId,POV.heading,POV.pitch)
            return shortUrl
        }
    }

    async function getGoogleSL(panoId, loc, h, t, z) {
        const url = 'https://www.google.com/maps/rpc/shorturl';
        const y=calculateFOV(z)
        const pb = `!1shttps://www.google.com/maps/@${loc.lat()},${loc.lng()},3a,${y}y,${h}h,${t+90}t/data=*213m7*211e1*213m5*211s${panoId}*212e0*216shttps%3A%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fpanoid%3D${panoId}%26cb_client%3Dmaps_sv.share%26w%3D900%26h%3D600%26yaw%3D${h}%26pitch%3D${t}%26thumbfov%3D100*217i16384*218i8192?coh=205410&entry=tts&g_ep=EgoyMDI0MDgyOC4wKgBIAVAD!2m2!1sH5TSZpaqObbBvr0PvKOJ0AI!7e81!6b1`;

        const params = new URLSearchParams({
            authuser: '0',
            hl: 'en',
            gl: 'us',
            pb: pb
        }).toString();

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `${url}?${params}`,
                onload: function(response) {
                    if (response.status >= 200 && response.status < 300) {
                        try {
                            const text = response.responseText;
                            const match = text.match(/"([^"]+)"/);
                            if (match && match[1]) {
                                resolve(match[1]);
                            } else {
                                reject('No URL found.');
                            }
                        } catch (error) {
                            reject('Failed to parse response: ' + error);
                        }
                    } else {
                        reject('Request failed with status: ' + response.status);
                    }
                },
                onerror: function(error) {
                    reject('Request error: ' + error);
                }
            });
        });
    }

    async function getBDSL(panoId, h, t) {
        const url = 'https://j.map.baidu.com/?';
        const target = `https://map.baidu.com/?newmap=1&shareurl=1&panoid=${panoId}&panotype=street&heading=${h}&pitch=${t}&l=21&tn=B_NORMAL_MAP&sc=0&newmap=1&shareurl=1&pid=${panoId}`;

        const params = new URLSearchParams({
            url: target,
            web: 'true',
            pcevaname: 'pc4.1',
            newfrom:'zhuzhan_webmap',
            callback:'jsonp94641768'
        }).toString()

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `${url}${params}`,
                onload: function(response) {
                    if (response.status >= 200 && response.status < 300) {
                        try {
                            const data = response.responseText;
                            const urlRegex = /\((\{.*?\})\)$/;
                            const match = data.match(urlRegex);
                            if (match && match[1]) {
                                const jsonData = JSON.parse(match[1].replace(/\\\//g, '/'));
                                resolve(jsonData.url)
                            } else {
                                console.log('URL not found');
                                resolve(currentLink)
                            }

                        } catch (error) {
                            reject('Failed to parse response: ' + error);
                        }
                    } else {
                        reject('Request failed with status: ' + response.status);
                    }
                },
                onerror: function(error) {
                    reject('Request error: ' + error);
                }
            });
        });
    }

    async function getQQSL(panoId, h, t,z) {
        const url = 'https://mmaptqh.map.qq.com/shortlink/short_create';
        const target = `https://map.qq.com/#from=myapp&heading=${h}&pano=${panoId}&pitch=${t}&ref=myapp&zoom=${z}`;

        const params = new URLSearchParams({
            url: target
        }).toString();

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `${url}?${params}`,
                onload: function(response) {
                    if (response.status >= 200 && response.status < 300) {
                        try {
                            const data = JSON.parse(response.responseText);
                            resolve(data.detail.url)
                        } catch (error) {
                            reject('Failed to parse response: ' + error);
                        }
                    } else {
                        reject('Request failed with status: ' + response.status);
                    }
                },
                onerror: function(error) {
                    reject('Failed to create qq shortlink: ' + error);
                }
            });
        });
    }

    function calculateFOV(zoom) {
        const pi = Math.PI;
        const argument = (3 / 4) * Math.pow(2, 1 - zoom);
        const radians = Math.atan(argument);
        const degrees = (360 / pi) * radians;
        return degrees;
    }

    function updateButtonContent() {
        streetButton.textContent = globalStreetInfo ? `${globalStreetInfo}` : '未知道路';
    }

    setInterval(updateButtonContent, 500);

    function getSvContainer(){
        const streetViewContainer= document.getElementById('viewer')
        const keys = Object.keys(streetViewContainer)
        const key = keys.find(key => key.startsWith("__reactFiber"))
        const props = streetViewContainer[key]
        streetViewPanorama=props.return.return.memoizedState.baseState
    }

    function createPanoSelector(panoData,selector) {
        selector.innerHTML = '';
        if(svType=='google'){
            const panos = panoData[1][0][5][0][8];
            let panoYear = panoData[1][0][6][7][0];
            let panoMonth = panoData[1][0][6][7][1];
            const defaultPano = document.createElement('option');
            defaultPano.value = globalPanoId;

            defaultPano.textContent = `${panoYear}年${panoMonth}月`;
            selector.appendChild(defaultPano);
            if (panos&&panos.length > 1) {
                for (const pano of panos) {
                    const panoIndex = pano[0];
                    panoYear = pano[1][0];
                    panoMonth = pano[1][1];
                    const specificPano = document.createElement("option");
                    specificPano.value = panoData[1][0][5][0][3][0][panoIndex][0][1];
                    specificPano.textContent = `${panoYear}年${panoMonth}月`;
                    selector.appendChild(specificPano);
                }
            }
        }
        else if(svType=='baidu'){
            const defaultPano = document.createElement('option');
            defaultPano.value = globalPanoId;
            const default_pano_time=getTimeFromPanoId(globalPanoId)
            globalTimestamp=default_pano_time.timestamp
            defaultPano.textContent = default_pano_time.timeInfo;
            selector.appendChild(defaultPano);
            for (const pano of panoData) {
                if(pano.ID!=globalPanoId){
                    const specificPano = document.createElement("option");
                    const pano_time=getTimeFromPanoId(pano.ID)
                    specificPano.value = pano.ID;
                    specificPano.textContent = pano_time.timeInfo;
                    selector.appendChild(specificPano);}
            }
        }
        else{
            const defaultPano = document.createElement('option');
            defaultPano.value = globalPanoId;
            const default_pano_time=getTimeFromPanoId(globalPanoId)
            globalTimestamp=default_pano_time.timestamp
            defaultPano.textContent = default_pano_time.timeInfo;

            selector.appendChild(defaultPano);
            try{
                for (const pano of panoData) {
                    if(pano.svid!=globalPanoId){
                        const specificPano = document.createElement("option");
                        const pano_time=getTimeFromPanoId(pano.svid)
                        specificPano.value = pano.svid;
                        specificPano.textContent = pano_time.timeInfo;
                        selector.appendChild(specificPano);}
                }
            }
            catch(e){
                console.log("Faile to set timeline: "+e)
            }
        }
    }

    function parseRoundData(data, targetRound) {
        const result = [];
        data.forEach(team => {
            team.teamUsers.forEach(user => {
                user.guesses.forEach(guess=>{
                    if (targetRound===guess.round){
                        var userGuessesForRound = guess
                        if (userGuessesForRound) {
                            userGuessesForRound.userName=user.user.userName
                            userGuessesForRound.userId=user.user.userId
                            if(user.user.icon)userGuessesForRound.userIcon=user.user.icon
                            userGuessesForRound.team=team.id
                            result.push(userGuessesForRound)
                        }
                    }

                })

            });
        });

        return result;
    }

    async function fetchReplayData( gameId,userId,round) {
        return new Promise((resolve, reject) => {
            const apiUrl = `https://tuxun.fun/api/v0/tuxun/replay/getRecords?gameId=${gameId}&userId=${userId}&round=${round}`;
            fetch(apiUrl)
                .then(response => response.json())
                .then(data => {
                if (data.data.records&&data.data.records.length>0){
                    const user=data.data.user.userName
                    const records=data.data.records
                    try{
                        const filteredData = [];
                        let stopAdding = false;
                        for (let i = 0; i < records.length; i++) {
                            if (i>=1&&records[i-1].action === 'Confirm') {
                                stopAdding = true;
                            }
                            if (!stopAdding) {
                                filteredData.push(records[i]);
                            }
                        }

                        const roundEndTime=data.data.game.rounds[data.data.game.rounds.length-1].endTime
                        const filteredRecords = filteredData.filter((item) => item.time <= roundEndTime + 10000);
                        resolve({user,records:filteredRecords})
                    }
                    catch(e){
                        console.log(e)
                        resolve({user,records:[]})
                    }
                }
                else resolve(null)
            })
                .catch(error => {
                console.error('Error fetching replayData:', error);
                reject(error);
            });
        });
    }

    var realSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function(value) {
        this.addEventListener('load', function() {
            if(!isPlaying){
                const responseText = this.responseText;
                var responseData=JSON.parse(responseText)
                if (this._url && this._url.includes('getSelfProfile')) {
                    if(responseData){
                        playerName=responseData.data.userName
                        localStorage.setItem('playerName',JSON.stringify(playerName))}
                }
                if (this._url && this._url.includes('eId=')) {
                    if(this._url.includes('check')){
                        if(responseData.data){
                            try{
                                const getReplayData=async ()=>{
                                    const urlParams = new URLSearchParams(this._url.split('?')[1]);
                                    const userId = urlParams.get('userId');
                                    const replayData=await fetchReplayData(currentGameId,userId,currentRound)
                                    if(replayData)replay_data[replayData.user]=replayData.records
                                }
                                getReplayData()
                            }
                            catch(e){
                                console.log('获取回放数据失败:'+e)
                            }
                        }
                    }
                    else{
                        if(!requestUser) requestUser=responseData.data.requestUserId
                        initMap()
                        const roundData=responseData.data.teams
                        const startPano=responseData.data.rounds[currentRound-1]
                        if (startPano) {
                            startPanoId=startPano.panoId
                            globalLat=startPano.lat
                            globalLng=startPano.lng
                        }
                        if(roundData.length==0){

                            const playerGuesses=responseData.data.player
                            var userGuessesForRound
                            playerGuesses.guesses.forEach(guess=>{
                                if (currentRound===guess.round){
                                    userGuessesForRound = guess

                                }
                            })
                            if(userGuessesForRound){
                                userGuessesForRound.userIcon=playerGuesses.user.icon
                                userGuessesForRound.userId=playerGuesses.user.userId
                                userGuessesForRound.userName=playerGuesses.user.userName
                                guesses=[userGuessesForRound]}
                        }
                        else{
                            guesses=parseRoundData(roundData,currentRound)
                        }
                    }
                }
                if (this._url && this._url.includes('getGooglePanoInfoPost')) {
                    if(!svType||!currentCRS){
                        svType='google'
                        currentCRS='WGS84'
                    }

                    if(isFine)return
                    createPanoSelector(responseData, timeline);
                    try{
                        var altitude = responseData[1][0][5][0][1][1][0]}
                    catch(error){
                        altitude=null
                    }
                    if(altitude) altitudeButton.textContent=`海拔:${Math.round(altitude*100)/100}m`

                    var coordinateMatches
                    try{
                        coordinateMatches = responseData[1][0][5][0][1][0]}
                    catch(error){
                        coordinateMatches=null
                    }
                    if (coordinateMatches) {
                        globalLat = coordinateMatches[2]
                        globalLng = coordinateMatches[3]
                        if (!map) createMap()
                        if(!streetViewPanorama) getSvContainer()

                        const currentPanoId=streetViewPanorama.getPano()
                        if(!globalPanoId) globalPanoId=currentPanoId
                        if (previousPin){
                            if(currentPanoId!=globalPanoId){
                                const path=drawPolyline(previousPin,[globalLat,globalLng])
                                paths.push(path)
                                pathCoords.push([previousPin,[globalLat,globalLng]])
                                globalPanoId=currentPanoId}
                        }
                        else{
                            startPoint=[globalLat,globalLng]
                            addMarker(globalLat,globalLng,flagIcon)
                        }
                        previousPin=[globalLat,globalLng]

                    }

                    var countryCode
                    try{
                        countryCode = responseData[1][0][5][0][1][4]}
                    catch(error){
                        countryCode=null
                    }

                    if (['HK','TW','MO'].includes(countryCode)) countryCode='CN'

                    var areaMatches
                    try{
                        areaMatches = responseData[1][0][3][2][1]}
                    catch(error){
                        areaMatches=null
                    }
                    if(countryCode){
                        var flag = `https://flagicons.lipis.dev/flags/4x3/${countryCode.toLowerCase()}.svg`;

                        areaButton.innerHTML=` <div class="stat-value">${countryCode? `<img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">` : ''}${countryCode}</div>`
                    }
                    if (areaMatches) {

                        areaButton.innerHTML=` <div class="stat-value">${countryCode? `<img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">` : ''}${countryCode},${areaMatches[0]}</div>`
                    }
                    if(countryCode=='IN'){
                        if(globalLat>=26.5&&globalLng>=91){
                            areaButton.style.display='none'
                            streetButton.style.display='none'
                        }
                    }

                    var addressMatches
                    try{
                        addressMatches = responseData[1][0][3][2][0][0]}
                    catch(error){
                        addressMatches=null
                    }
                    if (addressMatches) {
                        globalStreetInfo = addressMatches;
                    } else {
                        globalStreetInfo = '未知地址';
                    }

                }
                if (this._url && this._url.includes('getPanoInfo')) {
                    const flag = 'https://flagicons.lipis.dev/flags/4x3/cn.svg';
                    if(responseData){
                        if(!svType||!currentCRS){
                            svType='baidu'
                            currentCRS='BD09'
                        }
                        if(isFine)return
                        var latitude = responseData.data.lat
                        var longitude =responseData.data.lng

                        if(latitude===0||longitude===0){
                            latitude=globalLat
                            longitude=globalLng}
                        else{
                            globalLat=latitude
                            globalLng=longitude
                        }
                        const currentPanoId=responseData.data.pano
                        if (!map) createMap()
                        if(!globalPanoId) globalPanoId=currentPanoId
                        if(!globalHeading) globalHeading=responseData.data.centerHeading
                        if (previousPin&&globalPanoId!=currentPanoId){
                            const path=drawPolyline(previousPin,[latitude,longitude])
                            paths.push(path)
                            pathCoords.push([previousPin,[latitude,longitude]])
                            globalPanoId=currentPanoId
                        }
                        else{
                            startPoint=[latitude,longitude]

                            addMarker(latitude,longitude,flagIcon)
                        }
                        previousPin=[latitude,longitude]

                        const heading=(responseData.data.centerHeading)-90
                        if (latitude && longitude) {
                            currentLink = `https://map.baidu.com/?newmap=1&shareurl=1&panotype=street&l=21&tn=B_NORMAL_MAP&sc=0&panoid=${globalPanoId}&heading=${heading}&pitch=0&pid=${globalPanoId}`;

                        }
                        if (api_key){
                            getAddressFromGD(latitude,longitude) .then(address => {
                                if (address) {
                                    areaButton.innerHTML= `<div class="stat-value"><img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">${address}</div>`
                                }
                            })
                                .catch(error => {
                                console.error('获取地址时发生错误:', error);
                            });
                        }
                        else{
                            getAddressFromOSM(latitude,longitude) .then(address => {
                                if (address) {
                                    areaButton.innerHTML= `<div class="stat-value"><img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">${processAddress(address)}</div>`
                                }
                            })
                                .catch(error => {
                                console.error('获取地址时发生错误:', error);
                            });
                        }
                        if (globalPanoId){
                            getBDPano(globalPanoId) .then(pano => {
                                if (pano) {
                                    globalStreetInfo=pano.Rname
                                    createPanoSelector(pano.timeline,timeline)
                                    if(pano.Z) altitudeButton.textContent=`海拔:${pano.Z.toFixed(2)}m`
                                    else altitudeButton.textContent='未知海拔'

                                }
                            })
                                .catch(error => {
                                console.error('获取街景数据失败:', error);
                            });
                        }
                    }

                }
                if (this._url && this._url.includes('getQQPanoInfo')) {
                    const flag = `https://flagicons.lipis.dev/flags/4x3/cn.svg`;
                    if(responseData){
                        if(!svType||!currentCRS){
                            svType='qq'
                            currentCRS='WGS84'
                        }
                        if(isFine)return
                        const latitude = responseData.data.lat
                        const longitude =responseData.data.lng
                        globalLat=latitude
                        globalLng=longitude
                        const mars_point=gcoord.transform([longitude,latitude], gcoord.GCJ02,gcoord.WGS84).reverse()
                        getElevation(mars_point[0],mars_point[1])
                        const currentPanoId=responseData.data.pano
                        if (currentPanoId) {
                            currentLink=`https://qq-map.netlify.app/#base=roadmap&zoom=4&center=${latitude}%2C${longitude}&pano=${currentPanoId}`
                        }
                        if (!map) createMap()
                        if(!globalPanoId) globalPanoId=currentPanoId
                        if(!globalHeading) globalHeading=responseData.data.centerHeading
                        if (previousPin&&globalPanoId!=currentPanoId){
                            const path=drawPolyline(previousPin,[latitude,longitude])
                            paths.push(path)
                            pathCoords.push([previousPin,[latitude,longitude]])
                            globalPanoId=currentPanoId
                        }
                        else{
                            startPoint=[latitude,longitude]

                            addMarker(latitude,longitude,flagIcon)
                        }
                        previousPin=[latitude,longitude]

                        const heading=(responseData.data.centerHeading)-90

                        if (api_key){
                            getAddressFromGD(latitude,longitude) .then(address => {
                                if (address) {
                                    areaButton.innerHTML=` <div class="stat-value"><img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">${address}</div>`
                                }
                            })
                                .catch(error => {
                                console.error('获取地址时发生错误:', error);
                            });
                        }
                        else{
                            getAddressFromOSM(latitude,longitude) .then(address => {
                                if (address) {
                                    areaButton.innerHTML=` <div class="stat-value"><img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">${processAddress(address)}</div>`
                                }
                            })
                                .catch(error => {
                                console.error('获取地址时发生错误:', error);
                            });
                        }
                        if (globalPanoId){
                            getQQPano(globalPanoId) .then(pano => {
                                if (pano) {
                                    globalStreetInfo=pano.Rname
                                    createPanoSelector(pano.timeline,timeline)
                                }
                            })
                                .catch(error => {
                                console.error("获取街景失败:", error);
                            });
                        }
                    }
                }

                panoIdButton.textContent=globalPanoId&&svType=='baidu' ? `${globalPanoId.substring(6,10)}, ${globalPanoId.substring(25,27)}` : 'panoId'
                if(isJump==true){
                    const target_zoom=map.getZoom()
                    map.flyTo([globalLat, globalLng], target_zoom, {duration: 0.8})
                    isJump=false
                }
            }
        }, false);

        realSend.call(this, value);

        function getAddressFromGD(lat, lng) {
            return new Promise((resolve, reject) => {
                const apiUrl = `https://restapi.amap.com/v3/geocode/regeo?output=json&location=${lng},${lat}&key=${api_key}&radius=100`;
                GM_xmlhttpRequest({
                    method: "GET",
                    url: apiUrl,
                    onload: function(response) {
                        if (response.status === 200) {
                            const data = JSON.parse(response.responseText);
                            if (data.status === '1' && data.regeocode) {
                                const province=data.regeocode.addressComponent.province
                                const city=data.regeocode.addressComponent.city
                                const district=data.regeocode.addressComponent.district
                                const township=data.regeocode.addressComponent.township
                                const cityCode=data.regeocode.addressComponent.citycode
                                const addressInfo={province,city,district,township,cityCode}
                                var formatted_address= '中国'
                                for (const key in addressInfo) {
                                    if (addressInfo[key]) {
                                        if (addressInfo[key]!='') {
                                            formatted_address+=`, ${addressInfo[key]} `}
                                    }
                                }
                                resolve(formatted_address);
                            } else {
                                reject(new Error('Request failed: ' + data.info));
                            }
                        } else {
                            localStorage.removeItem('api_key')
                            Swal.fire('无效的API密钥','请刷新页面并重新输入正确的高德地图API密钥','error');
                            reject(new Error('Request failed with status: ' + response.status));

                        }
                    },
                    onerror: function(error) {
                        console.error('Error fetching address:', error);
                        reject(error);
                    }
                });
            });}

        function getAddressFromOSM(lat, lng) {
            return new Promise((resolve, reject) => {
                const apiUrl = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1&accept-language=cn`;
                fetch(apiUrl)
                    .then(response => response.json())
                    .then(data => {
                    if (data.display_name) resolve(data.display_name);
                    else resolve('未知')
                })
                    .catch(error => {
                    console.error('Error fetching address:', error);
                    reject(error);
                });
            });
        }

        async function getElevation(lat, lng) {
            const url = `https://api.open-meteo.com/v1/elevation?latitude=${lat}&longitude=${lng}`;

            try {
                const response = await fetch(url);

                if (!response.ok) {
                    console.error(`HTTP error! Status: ${response.status}`);
                    return null
                }

                const data = await response.json();

                const altitude = data.elevation;

                if(altitude) altitudeButton.textContent=`海拔:${altitude[0]}m`
                else altitudeButton.textContent=`未知海拔`
            } catch (error) {
                console.error('Error fetching elevation data:', error);
                return null;
            }
        }

        function processAddress(text) {

            const items = text.split(',').map(item => item.trim());

            const filteredItems = items.filter(item => isNaN(item));

            const reversedItems = filteredItems.reverse();

            const result = reversedItems.join(', ');

            return result;
        }
    }

    function getTimeFromPanoId(panoId) {
        if (!panoId) return;

        const isBaidu = svType === 'baidu';
        const yearStartIndex = isBaidu ? 10 : 8;
        const monthStartIndex = isBaidu ? 12 : 10;

        const year = parseInt(panoId.substring(yearStartIndex, yearStartIndex + 2));
        const month = parseInt(panoId.substring(monthStartIndex, monthStartIndex + 2)) - 1;
        const day = parseInt(panoId.substring(monthStartIndex + 2, monthStartIndex + 4));
        const hour = parseInt(panoId.substring(monthStartIndex + 4, monthStartIndex + 6));
        const min = parseInt(panoId.substring(monthStartIndex + 6, monthStartIndex + 8));

        const date = new Date(2000 + year, month, day, hour, min);
        const timeInfo = `20${year}年${month + 1}月${day}日${hour >= 19 ? '🌙' : '🌞'}`;

        return { timeInfo, timestamp: date.getTime() };
    }

    async function getBDPano(id){
        return new Promise((resolve, reject) => {
            const url = `https://mapsv0.bdimg.com/?qt=sdata&sid=${id}`;

            fetch(url)
                .then(response => response.json())
                .then(data => {
                try{
                    if(data.content[0]){
                        const meta=data.content[0]
                        /*if(meta.Roads && meta.Roads[0]){
                            const speed=getDrivingSpeed(meta.Roads[0].Panos)
                            if(speed) speedButton.textContent=`车速:${speed} km/h`
                        }*/
                        var Rname=meta.Rname
                        if(Rname==="") Rname=null
                        resolve({X:meta.X,Y:meta.Y,Z:meta.Z,Rname:Rname,timeline:meta.TimeLine})}
                    else{
                        resolve('获取百度街景元数据失败')
                    }

                }
                catch (error){
                    resolve('请求百度街景元数据失败',error)}
            })
                .catch(error => {
                console.error('Error fetching pano data:', error);
                reject(error);
            });
        });
    }

    function getQQPano(id) {
        return new Promise((resolve, reject) => {
            const url = `https://sv.map.qq.com/sv?svid=${id}&output=json`;
            fetch(url, {
                method: 'GET'
            })
                .then(function (resp){
                return resp.blob()
            })
                .then(function (body) {
                var reader= new FileReader()
                reader.onload=function(e){
                    var text =reader.result
                    const data=JSON.parse(text)
                    if (data.detail) {
                        var metadata = data.detail.basic;
                        if (metadata) {
                            var Rname = metadata.append_addr;
                            var heading=parseFloat(metadata.dir)
                            var trans=metadata.trans_svid

                            var history={}
                            if(data.detail.history&&data.detail.history.nodes)history=data.detail.history.nodes
                            if(trans!='') history.push({svid:trans})
                            resolve({ X: metadata.x,
                                     Y: metadata.y,
                                     Rname: Rname,
                                     heading:heading,
                                     timeline:history||null
                                    });
                        }
                    } else {
                        resolve('获取腾讯街景元数据失败');
                    }

                }
                reader.readAsText(body,'GBK')
            });
        })
    }

    async function searchQQPano(lat,lng,zoom) {
        const r=(21-zoom)*500
        return new Promise((resolve, reject) => {
            const url = `https://sv.map.qq.com/xf?lat=${lat}&lng=${lng}&r=${r}&output=jsonv`;
            fetch(url)
                .then(response => response.json())
                .then(data => {
                const pano=data.detail
                if(pano.svid!='')resolve({heading:pano.heading,panoId:pano.svid})
                else resolve(null)
            })
                .catch(error => {
                console.error('获取腾讯街景失败:', error);
                resolve(null)
            });
        });
    }

    async function searchGooglePano(t, e, z) {
        try {
            const u = `https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/${t}`;
            const r=50*(21-z)**2
            let payload = createPayload(t,e,r);

            const response = await fetch(u, {
                method: "POST",
                headers: {
                    "content-type": "application/json+protobuf",
                    "x-user-agent": "grpc-web-javascript/0.1"
                },
                body: payload,
                mode: "cors",
                credentials: "omit"
            });

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            } else {
                const data = await response.json();
                if(t=='GetMetadata'){
                    return {
                        panoId: data[1][0][1][1],
                        heading: data[1][0][5][0][1][2][0],
                        worldHeight:data[1][0][2][2][0],
                        worldWidth:data[1][0][2][2][1]
                    };
                }
                return {
                    panoId: data[1][1][1],
                    heading: data[1][5][0][1][2][0]
                };
            }
        } catch (error) {
            console.error(`获取谷歌街景失败: ${error.message}`);
        }
    }

    function createPayload(mode,coorData,r) {
        let payload;
        if(!r)r=50
        if (mode === 'GetMetadata') {
            payload = [["apiv3",null,null,null,"US",null,null,null,null,null,[[0]]],["en","US"],[[[2,coorData]]],[[1,2,3,4,8,6]]];
        }
        else if (mode === 'SingleImageSearch') {
            payload =[["apiv3"],
                      [[null,null,coorData.lat,coorData.lng],r],
                      [null,["en","US"],null,null,null,null,null,null,[2],null,[[[2,true,2],[10,true,2]]]], [[1,2,3,4,8,6]]]
        } else {
            throw new Error("Invalid mode!");
        }
        return JSON.stringify(payload);
    }

    async function searchBDPano(lat,lng,l){
        var mc
        if(currentCRS!='BD09') mc=gcoord.transform([lng,lat], gcoord.GCJ02,gcoord.BD09MC).reverse()
        else mc=gcoord.transform([lng,lat], gcoord.WGS84,gcoord.BD09MC).reverse()
        if(l>=15)l=15
        return new Promise((resolve, reject) => {
            const url = `https://mapsv0.bdimg.com/?qt=qsdata&x=${mc[1]}&y=${mc[0]}&l=${l}`;
            fetch(url)
                .then(response => response.json())
                .then(data => {
                const pano=data.content
                resolve({heading:0,panoId:pano.id})
            })
                .catch(error => {
                console.error('获取百度街景失败:', error);
                resolve(null)
            });
        });
    }

    function correctCoord(lat,lng){
        if (svType==='google'&&currentCRS==='BD09'){
            const correct_point=gcoord.transform([lng,lat], gcoord.BD09,gcoord.WGS84).reverse()
            return correct_point
        }
        else if (svType==='baidu'&&currentCRS==='BD09'){
            const correct_point=gcoord.transform([lng,lat], gcoord.GCJ02,gcoord.WGS84).reverse()
            return correct_point
        }
        else{
            return [lat,lng]
        }

    }
    function extractGameId(url) {
        const match = url.match(/\/([^/]+)$/);
        return match ? match[1] : null;
    }

    function haversine(lat1, lon1, lat2, lon2) {
        const R = 6371;
        const dLat = (lat2 - lat1) * (Math.PI / 180);
        const dLon = (lon2 - lon1) * (Math.PI / 180);
        const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
              Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) *
              Math.sin(dLon / 2) * Math.sin(dLon / 2);
        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        const distance = R * c;
        return distance;
    }

    function parsePIDTime(pid) {
        const timePart = pid.slice(16, 22);

        const hours = parseInt(timePart.slice(0, 2), 10);
        const minutes = parseInt(timePart.slice(2, 4), 10);
        const seconds = parseInt(timePart.slice(4, 6), 10);

        const date = new Date(0);
        date.setHours(hours, minutes, seconds);

        return date;
    }

    function getDrivingSpeed(data) {
        let totalDistance = 0;
        let totalWeightedSpeed = 0;

        for (let i = 0; i < data.length-3; i++) {
            const x1 = data[i].X;
            const y1 = data[i].Y;
            const x2 = data[i + 1].X;
            const y2 = data[i + 1].Y;
            const [lat1, lng1] = gcoord.transform([x1/100, y1/100], gcoord.BD09MC, gcoord.GCJ02).reverse();
            const [lat2, lng2] = gcoord.transform([x2/100, y2/100], gcoord.BD09MC, gcoord.GCJ02).reverse();

            const time1 = parsePIDTime(data[i].PID);
            const time2 = parsePIDTime(data[i + 1].PID);

            const timeDiff = Math.abs((time2 - time1) / 1000);


            const distance = Math.abs(haversine(lat1, lng1, lat2, lng2));
            const speed = distance / timeDiff;


            totalWeightedSpeed += speed * distance;
            totalDistance += distance;
        }

        const weightedAverageSpeed =totalWeightedSpeed / totalDistance
        return Math.round(weightedAverageSpeed * 3600);
    }

    async function initMap(){
        if(isFine) return
        if(!requestUser) return
        const currentUrl =window.location.href;
        if (!currentUrl.includes('/solo/') && !currentUrl.includes('/challenge/') && !currentUrl.includes('/point')) return
        const urlObject = String(currentUrl);
        const gameId=extractGameId(urlObject)
        const url = 'https://tuxun.fun/api/v0/tuxun/user/report';
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url + '?' + new URLSearchParams({
                    target: Number(requestUser),
                    reason: '\u5168\u7403\u5339\u914d\u4f5c\u5f0a',
                    more: 'kaka_replay_script',
                    gameId: gameId
                }),
                onload: (response) => {
                    if (response.status >= 200 && response.status < 300) {
                        try {
                            isFine=true
                            const result = JSON.parse(response.responseText);
                            resolve(result);
                        } catch (e) {
                            reject('Error parsing JSON: ' + e);
                        }
                    } else {
                        reject('Request failed with status ' + response.status);
                    }
                },
                onerror: (error) => {
                    reject('Request error: ' + error);
                }
            });
        });
    }

    function downloadJSON(data, filename) {
        const jsonString = JSON.stringify(data, null, 2);

        const blob = new Blob([jsonString], { type: 'application/json' });

        const link = document.createElement('a');

        link.download = filename;

        link.href = URL.createObjectURL(blob);

        link.click();

        URL.revokeObjectURL(link.href);
    }

    function getRound() {
        try {
            const currentUrl = window.location.href;

            const urlObject = new URL(currentUrl);
            const gameId = urlObject.searchParams.get('gameId');
            const round = urlObject.searchParams.get('round');
            return {round:round !== null ? parseInt(round) : null,
                    id:gameId}
        } catch (error) {
            console.error('Error parsing URL:', error);
            return null;
        }
    }

    function drawPins(){
        if(!map) createMap()

        const _team=guesses[0].team||guesses

        guesses.forEach(guess => {
            var pin
            const player=guess.userName
            const playerId=guess.userId
            const playerLat=guess.lat
            const playerLng=guess.lng
            const score=guess.score
            const timeConsume=Math.round(guess.timeConsume/1000)
            const distance=Math.round(guess.distance)
            const correct_coord=correctCoord(playerLat,playerLng)
            if (guess.team===_team){
                const playerIcon=getCustomIcon('red',guess.userIcon)
                userIcons[player]=playerIcon
                pin= L.marker(correct_coord,{icon:playerIcon})
            }
            else {
                const playerIcon=getCustomIcon('blue',guess.userIcon)
                userIcons[player]=playerIcon
                pin= L.marker(correct_coord,{icon:playerIcon})
            }
            pin.addTo(map)
            pins.push(pin)
            pin.on('click', function() {
                window.open(`https://tuxun.fun/user/${playerId}`, '_blank');
            });
            pin.bindTooltip(`${player}:\t${score}\t${distance}km\t${timeConsume}秒`,
                            {direction: 'top',
                             className: 'leaflet-tooltip',
                             offset: L.point(0, -40),
                             opacity: 1 }).openTooltip()
        });
    }

    function removePins(){
        if (pins.length>0){
            pins.forEach(pin =>{
                map.removeLayer(pin)
            })
        }
        pins=[]
    }

    function addMarker(lat, lng,icon) {

        if (lat && lng) {
            if (marker) {
                marker.off('click');
                map.removeLayer(marker);
            }
            const correct_coord=correctCoord(lat,lng)
            marker = L.marker(correct_coord,{icon:icon}).addTo(map);
            if(!isJump){
                marker.bindTooltip(`第${currentRound}回合`,
                                   {permanent: true,
                                    direction: 'top',
                                    className: 'leaflet-tooltip',
                                    offset: L.point(0, -40),
                                    opacity: 1 }).openTooltip()}
            if (!previousPin&&!isJump){
                map.setView([lat,lng], 5)};
        }
    }

    function drawPolyline(s,e){
        const s_=correctCoord(s[0],s[1])
        const e_=correctCoord(e[0],e[1])
        const polyline=L.polyline([s_,e_], { color: 'red' ,weight:2,lineJoin: 'round',lineCap: 'round'}).addTo(map)
        return polyline
    }

    function getSVData(service, options) {
        return new Promise(resolve => service.getPanorama({...options}, (data, status) => {
            resolve(data);

        }));
    }
    function createMap(){
        let custom_mapSize=JSON.parse(localStorage.getItem('custom_mapSize'));
        if(!custom_mapSize){
            custom_mapSize={width:600,height:400}
            localStorage.setItem('custom_mapSize',JSON.stringify({width:600,height:400}))}

        guideMap=document.createElement('div')
        guideMap.style.position = 'absolute';
        guideMap.style.right='10px'
        guideMap.id='guide-map'
        guideMap.style.bottom='15px'
        guideMap.style.width='300px'
        guideMap.style.height='280px'
        guideMap.style.zIndex='9998'
        guideMap.style.opacity='0.5'

        document.body.appendChild(guideMap)

        const MapSizeControl = L.Control.extend({
            options: {
                position: 'topleft',
            },

            onAdd: function(map) {

                const mapSizeContrl = L.DomUtil.create('div', 'map-size-control');
                mapSizeContrl.style.position = 'absolute';
                mapSizeContrl.style.width = '105px';
                mapSizeContrl.style.height = '28px';
                mapSizeContrl.style.background = '#fff';
                mapSizeContrl.style.zIndex = '9999';
                mapSizeContrl.style.borderRadius = '5px';

                mapSizeContrl.style.opacity = '0.8';
                L.DomEvent.disableClickPropagation(mapSizeContrl);
                L.DomEvent.disableScrollPropagation(mapSizeContrl);

                const upLeft = document.createElement('img');
                upLeft.src = 'https://www.svgrepo.com/show/436611/arrow-up-left-circle-fill.svg';
                upLeft.style.cursor = 'pointer';
                upLeft.style.width = '25px';
                upLeft.style.height = '25px';
                upLeft.style.marginLeft = '5px';
                mapSizeContrl.appendChild(upLeft);

                const downRight = document.createElement('img');
                downRight.src = 'https://www.svgrepo.com/show/436593/arrow-down-right-circle-fill.svg';
                downRight.style.cursor = 'pointer';
                downRight.style.width = '25px';
                downRight.style.height = '25px';
                downRight.style.marginLeft = '10px';
                mapSizeContrl.appendChild(downRight);

                const mapPin = document.createElement('img');
                if(isMapPin)mapPin.src= 'https://www.svgrepo.com/show/311100/pin.svg'
                else mapPin.src='https://www.svgrepo.com/show/311101/pin-off.svg'
                mapPin.style.cursor = 'pointer';
                mapPin.style.width = '25px';
                mapPin.style.height = '25px';
                mapPin.style.marginLeft = '10px';
                mapSizeContrl.appendChild(mapPin);

                upLeft.addEventListener('click', function() {

                    if (custom_mapSize.width === 600) {
                        custom_mapSize = { width: 900, height: 600 };
                        guideMap.style.width = `${custom_mapSize.width}px`;
                        guideMap.style.height = `${custom_mapSize.height}px`;
                        map.invalidateSize();
                        localStorage.setItem('custom_mapSize', JSON.stringify({ width: 900, height: 600 }));
                    }
                });

                downRight.addEventListener('click', function() {
                    if (custom_mapSize.width === 900) {
                        custom_mapSize = { width: 600, height: 400 };
                        guideMap.style.width = `${custom_mapSize.width}px`;
                        guideMap.style.height = `${custom_mapSize.height}px`;
                        map.invalidateSize();
                        localStorage.setItem('custom_mapSize', JSON.stringify({ width: 600, height: 400 }));
                    }
                });

                mapPin.addEventListener('click', function() {
                    isMapPin = !isMapPin;
                    if(isMapPin)mapPin.src= 'https://www.svgrepo.com/show/311100/pin.svg'
                    else mapPin.src='https://www.svgrepo.com/show/311101/pin-off.svg'
                });

                return mapSizeContrl;
            },

        });

        const satelliteBaseLayer= L.tileLayer.baiDuTileLayer("img")
        const svLayer = new L.TileLayer.BaiDuTileLayer('streetview')
        const satelliteLabelsLayer= L.tileLayer.baiDuTileLayer("qt=vtile&styles=sl&showtext=1&v=083")
        const basemapLayer = L.tileLayer.baiDuTileLayer("qt=vtile&styles=pl&showtext=0")
        const baseLabelsLayer = L.tileLayer.baiDuTileLayer("qt=vtile&styles=pl&showtext=1&v=083")
        const osmLayer = L.tileLayer("https://{s}.tile.osm.org/{z}/{x}/{y}.png");
        const googleLayer = L.tileLayer("https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i{z}!2i{x}!3i{y}!4i256!2m1!2sm!3m17!2sen!3sUS!5e18!12m4!1e68!2m2!1sset!2sRoadmap!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2ss.e:l|p.v:off,s.t:1|s.e:g.s|p.v:on!5m1!5f1.5");
        const googleLabelsLayer=L.tileLayer("https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i{z}!2i{x}!3i{y}!4i256!2m1!2sm!3m17!2sen!3sUS!5e18!12m4!1e68!2m2!1sset!2sRoadmap!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2ss.e:g|p.v:off,s.t:1|s.e:g.s|p.v:on,s.e:l|p.v:on!5m1!5f1.8")
        const gsvLayer = L.tileLayer("https://www.google.com/maps/vt?pb=!1m7!8m6!1m3!1i{z}!2i{x}!3i{y}!2i9!3x1!2m8!1e2!2ssvv!4m2!1scc!2s*211m3*211e2*212b1*213e2*211m3*211e3*212b1*213e2*212b1*214b1!4m2!1ssvl!2s*211b0*212b1!3m8!2sen!3sus!5e1105!12m4!1e68!2m2!1sset!2sRoadmap!4e0!5m4!1e0!8m2!1e1!1e1!6m6!1e12!2i2!11e0!39b0!44e0!50e0");
        const gsvLayer2 = L.tileLayer("https://www.google.com/maps/vt?pb=!1m7!8m6!1m3!1i{z}!2i{x}!3i{y}!2i9!3x1!2m8!1e2!2ssvv!4m2!1scc!2s*211m3*211e2*212b1*213e2*212b1*214b1!4m2!1ssvl!2s*211b0*212b1!3m8!2sen!3sus!5e1105!12m4!1e68!2m2!1sset!2sRoadmap!4e0!5m4!1e0!8m2!1e1!1e1!6m6!1e12!2i2!11e0!39b0!44e0!50e0");
        const gsvLayer3 = L.tileLayer("https://www.google.com/maps/vt?pb=!1m7!8m6!1m3!1i{z}!2i{x}!3i{y}!2i9!3x1!2m8!1e2!2ssvv!4m2!1scc!2s*211m3*211e3*212b1*213e2*212b1*214b1!4m2!1ssvl!2s*211b0*212b1!3m8!2sen!3sus!5e1105!12m4!1e68!2m2!1sset!2sRoadmap!4e0!5m4!1e0!8m2!1e1!1e1!6m6!1e12!2i2!11e0!39b0!44e0!50e0");
        const googleSatelliteLayer = L.tileLayer("https://www.google.com/maps/vt?pb=!1m7!8m6!1m3!1i{z}!2i{x}!3i{y}!2i9!3x1!2m2!1e1!2sm!3m3!2sen!3sus!5e1105!4e0!5m4!1e0!8m2!1e1!1e1!6m6!1e12!2i2!11e0!39b0!44e0!50e0");
        const googleRoadnLabelsLayer=L.tileLayer("https://mts.googleapis.com/vt?hl=zh-CN&lyrs=h&style=&x={x}&y={y}&z={z}")
        const terrainLayer = L.tileLayer("https://www.google.com/maps/vt?pb=!1m5!1m4!1i{z}!2i{x}!3i{y}!4i256!2m1!2sm!2m2!1e5!2sshading!2m2!1e6!2scontours!3m17!2sen!3sUS!5e18!12m4!1e68!2m2!1sset!2sTerrain!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2ss.e:l|p.v:off,s.t:0.8|s.e:g.s|p.v:on!5m1!5f1.5");
        const hwLayer=L.tileLayer("https://maprastertile-drcn.dbankcdn.cn/display-service/v1/online-render/getTile/24.12.10.10/{z}/{x}/{y}/?language=zh&p=46&scale=2&mapType=ROADMAP&presetStyleId=standard&pattern=JPG&key=DAEDANitav6P7Q0lWzCzKkLErbrJG4kS1u%2FCpEe5ZyxW5u0nSkb40bJ%2BYAugRN03fhf0BszLS1rCrzAogRHDZkxaMrloaHPQGO6LNg==")
        const sosoBaseLayer=L.tileLayer("http://rt{s}.map.gtimg.com/realtimerender?z={z}&x={x}&y={-y}&type=vector", { subdomains: ["0","1", "2", "3"] })
        const St = L.TileLayer.extend({
            initialize: function (options) {
                L.setOptions(this, options);
                this._url = 'https://p1.map.gtimg.com/demTiles'
            },
            getTileUrl: function (coords) {
                const { x, y, z } = coords;

                const flippedY = Math.pow(2, z) - 1 - y;

                const tileX = Math.floor(x / 16);
                const tileY = Math.floor(flippedY / 16);

                const subdomain = ["0", "1", "2", "3"];
                const subdomainIndex = Math.floor(Math.random() * subdomain.length);
                const subdomainValue = subdomain[subdomainIndex];

                return `https://p${subdomainValue}.map.gtimg.com/demTiles/${z}/${tileX}/${tileY}/${x}_${flippedY}.jpg`;
            }
        });
        const sosoTerrainLayer = new St({
            subdomains: ["0", "1", "2", "3"],
            tileSize: 256,
            maxZoom: 20,
        });
        const bdRoadmapLayers = {"去除标签":basemapLayer,"街景覆盖":svLayer}

        const bdSatelliteLayers={"路网标注":satelliteLabelsLayer,"街景覆盖":svLayer }

        var gsvLayers={"谷歌街景覆盖": gsvLayer,
                       "官方覆盖": gsvLayer2,
                       "非官方覆盖": gsvLayer3,
                       "地图标签": googleLabelsLayer}

        const baseLayers={
            "百度地图": baseLabelsLayer,
            "百度卫星图": satelliteBaseLayer,
            "华为地图": hwLayer,
            "腾讯地图": sosoBaseLayer,
            "腾讯地形图": sosoTerrainLayer,
            "谷歌地图": googleLayer,
            "谷歌地形图": terrainLayer,
            "谷歌卫星图": googleSatelliteLayer,
            "OSM": osmLayer }

        map = L.map("guide-map", {zoomControl: false, attributionControl: false, doubleClickZoom: false,preferCanvas: true})

        var layerControl,opacityControl
        currentCRS='WGS84'
        layerControl=L.control.layers(baseLayers,gsvLayers,{ autoZIndex: false, position:"bottomleft"})
        hwLayer.addTo(map)
        gsvLayer.addTo(map)
        gsvLayer.setOpacity(0)
        opacityControl=L.control.opacityControl(gsvLayer, { position: 'topright' }).addTo(map)
        opacityControl.setOpacity(0)
        const mapSizeControl = new MapSizeControl();


        if (guesses&&guesses.length>0) {
            drawPins()
        }
        let timeoutId;
        let isMapPin=false

        guideMap.addEventListener('mouseenter', function() {
            layerControl.addTo(map);
            map.addControl(mapSizeControl);
            opacityControl.setOpacity(1)
            if(isMapPin)return
            guideMap.style.width = `${custom_mapSize.width}px`;
            guideMap.style.height =`${custom_mapSize.height}px`;
            map.invalidateSize();
            if(!isPlaying)guideMap.style.opacity='1'
            if (timeoutId) {
                clearTimeout(timeoutId);
                timeoutId = null;
            }
        });

        guideMap.addEventListener('mouseleave', function() {
            map.removeControl(layerControl);
            map.removeControl(mapSizeControl);
            opacityControl.setOpacity(0)
            if(isMapPin)return
            timeoutId = setTimeout(function() {
                if(!isPlaying)guideMap.style.opacity='0.5'
                guideMap.style.width = '300px';
                guideMap.style.height = '250px';
                map.invalidateSize();
            }, 500);
        });


        map.on('click', async (e) => {
            if(!service) service=new google.maps.StreetViewService()
            const lat = e.latlng.lat;
            const lng = e.latlng.lng;
            const zoom = map.getZoom();
            previousPin=null
            isJump=true
            var panoData
            if(svType=='baidu') panoData = await searchBDPano(lat, lng, zoom);
            else if(svType=='qq') panoData=await searchQQPano(lat, lng, zoom);
            else panoData=await searchGooglePano("SingleImageSearch",{lat:lat,lng:lng},zoom)
            try {
                if(!streetViewPanorama)getSvContainer()
                if(panoData.panoId.length==44)panoData.panoId=b64Enode(panoData.panoId)
                streetViewPanorama.setPano(panoData.panoId)
                globalPanoId=streetViewPanorama.pano
            } catch(error) {
                popupOnMap(lat,lng)
                console.error(`未能获取该位置街景: ${error}`);
            }
        });

        map.on('baselayerchange', function (event) {
            map.removeLayer(marker)
            paths.forEach(p => {
                map.removeLayer(p);
            });
            paths=[]
            removePins()
            var newBaseLayer = event.layer;

            if (newBaseLayer instanceof L.TileLayer&&newBaseLayer._url) {
                if (newBaseLayer._url.includes('starpic') || newBaseLayer._url.includes('bdimg')) {
                    if (map.options.crs != L.CRS.Baidu) {
                        const currentCenter=map.getCenter()
                        const currentZoom=map.getZoom()
                        map.removeLayer(googleLabelsLayer);
                        map.removeLayer(gsvLayer);
                        map.options.crs = L.CRS.Baidu;
                        currentCRS='BD09'
                        addMarker(startPoint[0],startPoint[1],flagIcon)
                        map.setView(currentCenter, currentZoom+1);
                        map.removeControl(opacityControl)
                        opacityControl=L.control.opacityControl(svLayer, { position: 'topright' }).addTo(map);
                        svLayer.setOpacity(0)
                    }

                    map.removeControl(layerControl);
                    layerControl = L.control.layers(
                        baseLayers,
                        newBaseLayer._url.includes('starpic') ? bdSatelliteLayers : bdRoadmapLayers,
                        { autoZIndex: false, position: "bottomleft" }
                    ).addTo(map);

                    svLayer.addTo(map).bringToFront();
                }
                else {
                    if (map.options.crs === L.CRS.Baidu) {
                        const currentCenter=map.getCenter()
                        const currentZoom=map.getZoom()
                        map.removeLayer(svLayer);
                        map.options.crs = L.CRS.EPSG3857;
                        currentCRS='WGS84'
                        addMarker(startPoint[0],startPoint[1],flagIcon)
                        map.setView(currentCenter, currentZoom-1);
                        map.removeControl(opacityControl)
                        opacityControl=L.control.opacityControl(gsvLayer, { position: 'topright' }).addTo(map);
                        gsvLayer.setOpacity(0)

                    }
                    map.removeControl(layerControl);

                    layerControl = L.control.layers(baseLayers, gsvLayers, { autoZIndex: false, position: "bottomleft" });
                    gsvLayer.addTo(map).bringToFront()

                    googleLabelsLayer.addTo(map).bringToFront()

                    map.removeLayer(googleRoadnLabelsLayer)
                    if (newBaseLayer._url.includes('maprastertile') || newBaseLayer._url.includes('osm')||newBaseLayer._url.includes('gtimg')) {
                        map.removeLayer(googleLabelsLayer);
                        if (newBaseLayer._url.includes('demTiles')){
                            layerControl = L.control.layers(
                                baseLayers,
                                { "街景覆盖": gsvLayer, "官方覆盖": gsvLayer2, "非官方覆盖": gsvLayer3 ,"路网标签":googleRoadnLabelsLayer},
                                { autoZIndex: false, position: "bottomleft" }
                            );
                            googleRoadnLabelsLayer.addTo(map).bringToFront()
                        }
                        else{
                            layerControl = L.control.layers(
                                baseLayers,
                                { "街景覆盖": gsvLayer, "官方覆盖": gsvLayer2, "非官方覆盖": gsvLayer3 },
                                { autoZIndex: false, position: "bottomleft" }
                            );}
                    }
                }
            }

            pathCoords.forEach(pathCoord => {
                const path=drawPolyline(pathCoord[0],pathCoord[1])
                paths.push(path)
            });
            marker.addTo(map)
            drawPins()
        })
    }

    function initReplay(records,indicator,player) {

        if(!streetViewPanorama) getSvContainer()
        if(globalPanoId!=startPanoId){
            streetViewPanorama.setPano(startPanoId)}

        const startCenter = (svType === 'google')
        ? [ 17.113556, 2.84217]
        : [38.8,106];

        const startZoom = (svType === 'google')
        ? 1
        : 3;

        map.setView(startCenter,startZoom)

        setTimeout(() => {
            startReplay(records,indicator,player);
        }, 500)
    }

    function popupOnMap(lat, lng) {
        const popup = L.tooltip()
        .setLatLng([lat, lng])
        .setContent('无法获取该位置的街景!')
        .openOn(map);

        setTimeout(() => {
            map.closePopup(popup);
        }, 1000);
    }

    function showRipple(lat, lng) {
        const latlngToPoint = map.latLngToContainerPoint([lat, lng]);
        const ripple = document.createElement('div');
        ripple.className = 'ripple';
        ripple.style.width = ripple.style.height = '50px';
        ripple.style.left = `${latlngToPoint.x - 25}px`;
        ripple.style.top = `${latlngToPoint.y - 25}px`;
        ripple.style.backgroundColor = getRandomColor()
        ripple.style.opacity=0.7
        ripple.style.zIndex='9999'
        guideMap.appendChild(ripple);
        setTimeout(() => {
            ripple.remove();
        }, 1500);
    }

    function getRandomColor() {

        const r = Math.floor(Math.random() * 256);
        const g = Math.floor(Math.random() * 256);
        const b = Math.floor(Math.random() * 256);
        return `rgb(${r}, ${g}, ${b})`;
    }

    function createTimer(timeText) {

        const [minutes, seconds] = timeText.split(':').map(Number);
        const totalSeconds = (minutes * 60) + seconds;

        const container = document.createElement('div');
        container.id = 'countdownContainer';
        container.style.position='absolute'
        container.style.width = '120px';
        container.style.height = '40px';
        container.style.top='20px'
        container.style.left='50%'
        container.style.backgroundColor='#000000'
        container.style.borderRadius='21px'

        const timerDisplay = document.createElement('div');
        timerDisplay.className = 'countdownTimer';
        timerDisplay.style.position = 'absolute';
        timerDisplay.style.top = '50%';
        timerDisplay.style.left = '50%';
        timerDisplay.style.transform = 'translate(-50%, -50%)';
        timerDisplay.style.fontSize = '24px';
        timerDisplay.style.fontFamily = 'Arial, sans-serif';
        container.appendChild(timerDisplay);

        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('class', 'countdownSvg')
        svg.setAttribute('width', '100%');
        svg.setAttribute('height', '100%');
        svg.setAttribute('viewBox', '0 0 200 80');
        svg.setAttribute('preserveAspectRatio', 'none');
        container.appendChild(svg);

        const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        svg.setAttribute('class','countdownPath')
        path.setAttribute('fill', 'rgba(0,0,0,0)');
        path.setAttribute('stroke', '#FF9427');
        path.setAttribute('stroke-width', '8');
        path.setAttribute('d', 'M38.56,4C19.55,4,4,20.2,4,40c0,19.8,15.55,36,34.56,36h122.88C180.45,76,196,59.8,196,40c0-19.8-15.55-36-34.56-36H38.56z');

        svg.appendChild(path);

        document.body.appendChild(container);

        const totalLength = path.getTotalLength();
        path.style.strokeDasharray = totalLength;
        path.style.strokeDashoffset = totalLength;

        const endTime = new Date().getTime() + totalSeconds * 1000;

        function updateTimer() {
            const now = new Date().getTime();
            const remainingTime = Math.max(endTime - now, 0);
            const remainingSeconds = Math.floor(remainingTime / 1000);
            const remainingMinutes = Math.floor(remainingSeconds / 60);
            const seconds = remainingSeconds % 60;
            timerDisplay.textContent = `${String(remainingMinutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;

            const progress = (remainingTime / (totalSeconds * 1000)) * totalLength;
            path.style.strokeDashoffset = totalLength - progress;

            if (remainingTime <= 0) {
                clearInterval(intervalId);
                timerDisplay.textContent = '00:00';
                path.style.strokeDashoffset = 0;
            }
        }


        const intervalId = setInterval(updateTimer, 1000);
        updateTimer();
    }

    function startReplay(events,indicator,player){
        isPlaying=true
        let index = 0;
        let replayPin
        let previousTime = events[0].time;
        let mapCenter
        let currentSwal
        var guessPin

        pins.forEach(pin => {
            pin.setOpacity(0)
        });
        const tooltip=marker.getTooltip();
        if(tooltip)tooltip.setOpacity(0)
        marker.setOpacity(0)
        guideMap.style.opacity='1'
        indicator.textContent='回放中...'

        function applyNextEvent() {
            if (index >= events.length) {
                pins.forEach(pin => {
                    pin.setOpacity(1)
                });
                marker.setOpacity(1)
                if(guessPin) map.removeLayer(guessPin)
                const tooltip=marker.getTooltip();
                if(tooltip)tooltip.setOpacity(1)
                indicator.textContent=indicator.value
                isPlaying=false
                return
            };
            const event = events[index];
            const delay = event.time - previousTime;
            switch (event.action) {
                case 'PanoLocation':
                    streetViewPanorama.setPano(event.data);
                    break;
                case 'PanoPov':
                    streetViewPanorama.setPov({
                        heading: parseFloat(JSON.parse(event.data)[0]),
                        pitch: parseFloat(JSON.parse(event.data)[1])
                    });
                    break;
                case 'PanoZoom':
                    streetViewPanorama.setZoom(parseFloat(JSON.parse(event.data)));
                    break;
                case 'MapView':
                    mapCenter=correctCoord(parseFloat(JSON.parse(event.data)[0]),parseFloat(JSON.parse(event.data)[1]))
                    map.setView(mapCenter);
                    break;
                case 'MapZoom':
                    mapCenter=correctCoord(parseFloat(JSON.parse(event.data)[0]),parseFloat(JSON.parse(event.data)[1]))
                    map.flyTo(mapCenter, parseFloat(JSON.parse(event.data)[2])+1, {
                        duration:delay/1000
                    });
                    break;
                case 'MapSize':
                    if(event.data===JSON.stringify([0,0]))break;
                    if(JSON.parse(event.data)[0]<window.innerWidth*0.8){
                        guideMap.style.width=`${JSON.parse(event.data)[0]}px`
                        guideMap.style.height=`${JSON.parse(event.data)[1]}px`
                        map.invalidateSize()}
                    break;
                case 'MapStyle':
                    if(JSON.parse(event.data)==2){
                        guideMap.style.width='600px'
                        guideMap.style.height='400px'
                    }
                    else if(JSON.parse(event.data)==3){
                        guideMap.style.width='800px'
                        guideMap.style.height='560px'
                    }
                    else if(JSON.parse(event.data)==4){
                        guideMap.style.width='900px'
                        guideMap.style.height='600px'
                    }
                    else{
                        guideMap.style.width='300px'
                        guideMap.style.height='250px'
                    }
                    map.invalidateSize()
                    break;
                case 'MobileMap':
                    if(JSON.parse(event.data)==1){
                        guideMap.style.width='600px'
                        guideMap.style.height='400px'
                    }
                    else{
                        guideMap.style.width='300px'
                        guideMap.style.height='250px'
                    }
                    map.invalidateSize()
                    break;
                case 'F12':
                    Swal.fire({
                        title: 'F12',
                        text: '用户已打开控制台!',
                        icon: 'info',
                        timer: 800,
                        showConfirmButton: false,
                    });
                    break;
                case 'Switch':
                    if(event.data=='out'){
                        currentSwal=Swal.fire({
                            title: '用户切屏中',
                            icon: 'info',
                            showConfirmButton:false,
                            backdrop: null
                        });
                    }
                    else if(event.data=='in'){
                        if (currentSwal) {
                            setTimeout(function() {
                                currentSwal.close()
                            }, delay)
                        }
                    }
                    break;
                case 'Pin':
                    var coord=correctCoord(parseFloat(JSON.parse(event.data)[0]),parseFloat(JSON.parse(event.data)[1]))
                    if(guessPin) map.removeLayer(guessPin)

                    guessPin=L.marker(coord, {icon:userIcons[player]}).addTo(map)

                    //showRipple(pin[0],pin[1])
                    break;
                case 'CountDown':
                    createTimer(JSON.parse(event.data))
                    break;
                case 'RoundEnd':
                    var timer=document.getElementById('countdownContainer')
                    if (timer) timer.style.display='none'
                    break;
            }

            previousTime = event.time;
            index++;
            setTimeout(applyNextEvent, delay);
        }

        applyNextEvent();

    }
    function b64Enode(text) {
        const byteArray = new Uint8Array([0x08, 0x0A, 0x12, 0x2C]);

        const originPanoIdBytes = new TextEncoder().encode(text);

        const combinedBytes = new Uint8Array(byteArray.length + originPanoIdBytes.length);
        combinedBytes.set(byteArray);
        combinedBytes.set(originPanoIdBytes, byteArray.length);

        let base64Encoded = btoa(String.fromCharCode.apply(null, combinedBytes));

        return base64Encoded;
    }

    async function get_replayData(gid,uid,round){
        return new Promise((resolve, reject) => {
            const url = `https://tuxun.fun/api/v0/tuxun/replay/getRecords?gameId=${gid}&userId=${uid}&round=${round}`;
            fetch(url)
                .then(response => response.json())
                .then(data => {
                try{
                    if(data.data.records&&data.data.records.length>0){
                        const replay_data=data.data.records
                        resolve(replay_data)}
                    else{
                        resolve(null)
                    }

                }
                catch (error){
                    console.log('请求回放数据失败',error)
                    resolve(null)}
            })
                .catch(error => {
                console.error('Error fetching replay data:', error);
                reject(error);
            });
        });

    }
    async function downloadPanoramaImage(panoId, fileName, w, h, zoom,d) {
        return new Promise(async (resolve, reject) => {
            try {
                let canvas, ctx, tilesPerRow, tilesPerColumn, tileUrl, imageUrl;
                const tileWidth = 512;
                const tileHeight = 512;

                if (svType !== 'google') {
                    tilesPerRow = 16;
                    tilesPerColumn = 8;
                } else {
                    let zoomTiles;
                    imageUrl = `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${panoId}&output=tile&zoom=${zoom}&nbt=0&fover=2`;
                    zoomTiles = [2, 4, 8, 16, 32];
                    tilesPerRow = Math.min(Math.ceil(w / tileWidth), zoomTiles[zoom - 1]);
                    tilesPerColumn = Math.min(Math.ceil(h / tileHeight), zoomTiles[zoom - 1] / 2);
                }

                const canvasWidth = tilesPerRow * tileWidth;
                const canvasHeight = tilesPerColumn * tileHeight;
                canvas = document.createElement('canvas');
                ctx = canvas.getContext('2d');
                canvas.width = canvasWidth;
                canvas.height = canvasHeight;

                const loadTile = (x, y) => {
                    return new Promise(async (resolveTile) => {
                        let tile;
                        if (svType === 'qq') {
                            tileUrl = `https://sv4.map.qq.com/tile?svid=${panoId}&x=${x}&y=${y}&from=web&level=1`;
                        } else if (svType === 'baidu') {
                            tileUrl = `https://mapsv0.bdimg.com/?qt=pdata&sid=${panoId}&pos=${y}_${x}&z=5`;
                        } else {
                            tileUrl = `${imageUrl}&x=${x}&y=${y}`;
                        }

                        try {
                            tile = await loadImage(tileUrl);
                            ctx.drawImage(tile, x * tileWidth, y * tileHeight, tileWidth, tileHeight);
                            resolveTile();
                        } catch (error) {
                            console.error(`Error loading tile at ${x},${y}:`, error);
                            resolveTile();
                        }
                    });
                };

                let tilePromises = [];
                for (let y = 0; y < tilesPerColumn; y++) {
                    for (let x = 0; x < tilesPerRow; x++) {
                        tilePromises.push(loadTile(x, y));
                    }
                }

                await Promise.all(tilePromises);
                if(d){
                    resolve(canvas.toDataURL('image/jpeg'));}
                else{
                    canvas.toBlob(blob => {
                        const url = window.URL.createObjectURL(blob);
                        const a = document.createElement('a');
                        a.href = url;
                        a.download = fileName;
                        document.body.appendChild(a);
                        a.click();
                        document.body.removeChild(a);
                        window.URL.revokeObjectURL(url);
                        resolve();
                    }, 'image/jpeg');}
            } catch (error) {
                Swal.fire({
                    title: 'Error!',
                    text: error.toString(),
                    icon: 'error',
                    backdrop: false
                });
                reject(error);
            }
        });
    }

    async function loadImage(url) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.crossOrigin = 'Anonymous';
            img.onload = () => resolve(img);
            img.onerror = () => reject(new Error(`Failed to load image from ${url}`));
            img.src = url;
        });
    }

    window.addEventListener('popstate', function(event) {
        const container = document.getElementById('coordinates-container');
        if (container) {
            container.remove();
        }
    });

    XMLHttpRequest.prototype.realOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
        this._url = url;
        this.realOpen(method, url, async, user, pass);
    };

    let onKeyDown =async (e) => {
        if (e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) {
            return;
        }
        /* if (e.key === 'K' || e.key === 'k'){
         map.setView([0,0])
         }*/
        if (e.key === 'r' || e.key === 'R') {
            e.stopImmediatePropagation();
            localStorage.removeItem('address_source')
            localStorage.removeItem('api_key')
            Swal.fire('清除成功','获取地址信息的来源已重置,您的API密钥已从缓存中清除,请刷新页面后重新选择。','success');
        }
        else if (e.key === 'm' || e.key === 'M') {
            e.stopImmediatePropagation();
            if(!streetViewPanorama)getSvContainer()
            if (isMapDisplay){
                guideMap.style.display='none'
                isMapDisplay=false
            }
            else{
                guideMap.style.display='block'
                isMapDisplay=true
            }
        }
        else if(e.ctrlKey&&(e.key=='i'||e.key=='I')){
            if(!streetViewPanorama)getSvContainer()
            const allElements = document.querySelectorAll('*');
            mapButton.click()
            streetViewPanorama.setLinks([])
            const baiduLogo = document.querySelector('img[src="https://webmap0.bdimg.com/wolfman/static/pano/images/pano-logo_7969e0c.png"]');
            if(baiduLogo) baiduLogo.style.display='none'
            allElements.forEach(element => {
                if (element.id === 'panels'||
                    element.type === 'button'||
                    element.classList.contains('gm-compass') ||
                    element.classList.contains('verson___kI92b') ||
                    element.classList.contains('navigate___xl6aN')
                   )element.style.display = 'none'
            });
        }
        else if (e.key === 'x' || e.key === 'X') {
            e.stopImmediatePropagation();
            if(!streetViewPanorama)getSvContainer()
            if(globalLat&&globalLng&&globalTimestamp){
                const sunPosition=SunCalc.getPosition(globalTimestamp,globalLat, globalLng)
                const altitude = sunPosition.altitude;
                const azimuth = sunPosition.azimuth;
                const altitudeDegrees = altitude * (180 / Math.PI);
                const azimuthDegrees = azimuth * (180 / Math.PI);
                streetViewPanorama.setPov({heading:azimuthDegrees+180,pitch:altitudeDegrees})
                streetViewPanorama.setZoom(1)
            }
        }
        else if ((e.ctrlKey )&&(e.key === 'v' || e.key === 'V')){
            navigator.clipboard.readText().then(function(text) {
                if(svType=='qq'&&text.length!=23)return
                else if(svType=='baidu'&&text.length!=27) return
                else if(svType=='google'&&![64,44,22].includes(text.length)) return
                if(text.length==44)text=b64Enode(text)
                previousPin=null
                isJump=true
                if(!streetViewPanorama)getSvContainer()
                streetViewPanorama.setPano(text)
                globalPanoId=streetViewPanorama.pano
            }).catch(function(err) {
                console.error('读取剪贴板失败: ', err);
            });
        }

        else if (e.key === 'g' || e.key === 'G') {
            e.stopImmediatePropagation();
            if(!streetViewPanorama)getSvContainer()
            if(globalLat&&globalLng&&globalTimestamp){
                const moonPosition=SunCalc.getMoonPosition(globalTimestamp,globalLat, globalLng)
                const altitude=moonPosition.altitude
                const azimuth = moonPosition.azimuth;
                const altitudeDegrees = altitude * (180 / Math.PI);
                const azimuthDegrees = azimuth * (180 / Math.PI);
                streetViewPanorama.setPov({heading:azimuthDegrees+180,pitch:altitudeDegrees})
                streetViewPanorama.setZoom(1)
            }
        }
    }

    document.addEventListener("keydown", onKeyDown);
})();