// ==UserScript== // @name 图寻复盘工具 PRO // @namespace https://greasyfork.org/users/1179204 // @version 1.3.7 // @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/leaflet@1.9.2/dist/leaflet.js // @require https://unpkg.com/gcoord/dist/gcoord.global.prod.js // @run-at document-start // @downloadURL none // ==/UserScript== (function() { 'use strict'; GM_addStyle(` @import url('https://unpkg.com/leaflet@1.9.2/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; }; .link-button { background: none!important; border: none; padding: 0!important; color: #FFCC00 !important; text-decoration: underline; cursor: pointer; } .link-button:hover { color: #FFCC00 !important; } .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'); 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 = ` `; 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; } }); 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: `
`, iconSize: [30, 42], iconAnchor: [15, 42], popupAnchor: [1, -34], shadowSize: [42, 42] }); } const greenIcon = new L.Icon({ iconUrl: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41] }); const redIcon = new L.Icon({ iconUrl: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41] }); const blueIcon = new L.Icon({ iconUrl: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-blue.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41] }); const flagIcon = new L.divIcon({ className: 'custom-icon', html: `
`, iconSize: [36, 44], iconAnchor: [18, 44], popupAnchor: [1, -34], }); let guideMap,map,marker,pins=[],pathCoords=[],paths=[],svType,previousPin,currentCRS,startPoint,streetViewPanorama,isMapDisplay=true let api_key=JSON.parse(localStorage.getItem('api_key')); let address_source=JSON.parse(localStorage.getItem('address_source')); let replay_data=JSON.parse(localStorage.getItem('replay_data')); let playerName=JSON.parse(localStorage.getItem('playerName')) if (!address_source) { Swal.fire({ title: '请选择获取地址信息的来源', icon: 'question', 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, 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: '', 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') } }); } let currentRound=getRound().round let currentGameId=getRound().id 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); let currentLink = ''; let globalPanoId=null 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 = 'Area'; container.appendChild(areaButton); const streetButton = document.createElement('button'); streetButton.textContent = 'Street'; container.appendChild(streetButton); const altitudeButton = document.createElement('button'); altitudeButton.textContent = 'Altitude'; container.appendChild(altitudeButton); const timeButton = document.createElement('button'); timeButton.textContent = 'Date'; container.appendChild(timeButton); const panoIdButton = document.createElement('button'); panoIdButton.textContent = 'PanoId'; container.appendChild(panoIdButton); if (replay_data&&replay_data[currentGameId]){ var replayData=replay_data[currentGameId][currentRound]} if (replayData){ const replayButton = document.createElement('button'); replayButton.textContent = '开始回放'; replayButton.onclick = () =>{ initReplay() } container.appendChild(replayButton); const downloadButton=document.createElement('button'); downloadButton.textContent = '下载回放数据'; downloadButton.onclick = () =>{ downloadJSON(replay_data,'回放数据') } container.appendChild(downloadButton); const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json'; fileInput.style.display = 'none'; document.body.appendChild(fileInput); fileInput.addEventListener('change', function(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function() { try { const jsonData = JSON.parse(reader.result); replay_data=jsonData localStorage.setItem('replay_data',JSON.stringify(replay_data)) alert('回放数据已更新'); } catch (error) { alert('无效的JSON文件'); } }; reader.onerror = function() { alert('读取JSON文件失败'); }; reader.readAsText(file); }); const uploadButton=document.createElement('button'); uploadButton.textContent = '上传回放数据'; uploadButton.onclick = () =>{ fileInput.click() } container.appendChild(uploadButton); const deleteButton=document.createElement('button'); deleteButton.textContent = '删除此轮回放'; deleteButton.onclick = () =>{ delete replay_data[currentGameId][currentRound] setTimeout(function() { deleteButton.textContent='删除成功!' }, 100) setTimeout(function() { deleteButton.textContent='删除此轮回放' }, 1600) localStorage.setItem('replay_data',JSON.stringify(replay_data)) } container.appendChild(deleteButton); const clearButton=document.createElement('button'); clearButton.textContent = '删除所有回放'; clearButton.onclick = () =>{ localStorage.removeItem('replay_data') setTimeout(function() { clearButton.textContent='删除成功!' }, 100) setTimeout(function() { clearButton.textContent='删除所有回放' }, 1600) } container.appendChild(clearButton); } let globalTimeInfo = null; let globalAreaInfo = null; let globalStreetInfo = null; let guesses,startPanoId 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 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=13&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); } }); }); } 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}` : '路名'; timeButton.textContent = globalTimeInfo ? `${globalTimeInfo}` : '时间'; panoIdButton.textContent=globalPanoId ? `${globalPanoId.substring(6,10)}, ${globalPanoId.substring(25,27)}` : '未知' panoIdButton.style.fontSize='15px' } 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 parseRoundData(data, targetRound) { const result = []; data.forEach(team => { team.teamUsers.forEach(user => { const userGuessesForRound = user.guesses[targetRound-1]; if (userGuessesForRound) { userGuessesForRound.userName=user.user.userName userGuessesForRound.userId=user.user.userId userGuessesForRound.userIcon=user.user.icon userGuessesForRound.team=team.id result.push(userGuessesForRound) } }); }); return result; } var realSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function(value) { this.addEventListener('load', function() { var responseData if (this._url && this._url.includes('getSelfProfile')) { const responseText = this.responseText; if (responseText) responseData=JSON.parse(responseText) if(responseData){ playerName=responseData.data.userName localStorage.setItem('playerName',JSON.stringify(playerName))} } if (this._url && this._url.includes('Id=')) { const responseText = this.responseText; if (responseText) responseData=JSON.parse(responseText) const roundData=responseData.data.teams const startPano=responseData.data.rounds[currentRound-1] if (startPano) { startPanoId=startPano.panoId } if(roundData.length==0){ const playerGuesses=responseData.data.player var userGuessesForRound playerGuesses.guesses.forEach(guess=>{ if (currentRound===guess.round){ userGuessesForRound = guess} }) 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' panoIdButton.style.display='none' } const responseText = this.responseText; const panoData=JSON.parse(responseText) try{ var altitude = panoData[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 = panoData[1][0][5][0][1][0]} catch(error){ coordinateMatches=null } if (coordinateMatches) { const latitude = coordinateMatches[2] const longitude = 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,[latitude,longitude]) paths.push(path) pathCoords.push([previousPin,[latitude,longitude]]) globalPanoId=currentPanoId} } else{ startPoint=[latitude,longitude] addMarker(latitude,longitude,flagIcon) } previousPin=[latitude,longitude] } var countryCode try{ countryCode = panoData[1][0][5][0][1][4]} catch(error){ countryCode=null } if (countryCode==='HK'||countryCode==='TW'||countryCode==='MO') countryCode='CN' var areaMatches try{ areaMatches = panoData[1][0][3][2][1]} catch(error){ areaMatches=null } if(countryCode){ var flag = `https://flagicons.lipis.dev/flags/4x3/${countryCode.toLowerCase()}.svg`; areaButton.innerHTML=`
${countryCode? `` : ''}${countryCode}
` } if (areaMatches) { areaButton.innerHTML=`
${countryCode? `` : ''}${countryCode},${areaMatches[0]}
` } var addressMatches try{ addressMatches = panoData[1][0][3][2][0][0]} catch(error){ addressMatches=null } if (addressMatches) { globalStreetInfo = addressMatches; } else { globalStreetInfo = '未知地址'; } var timeMatches try{ timeMatches = panoData[1][0][6][7]} catch(error){ timeMatches=null } if (timeMatches) { globalTimeInfo = `${timeMatches[0]}年${timeMatches[1]}月`; } else { globalTimeInfo = '未知时间'; } } if (this._url && this._url.includes('getPanoInfo')) { const flag = `https://flagicons.lipis.dev/flags/4x3/cn.svg`; const responseText = this.responseText; if (responseText) responseData=JSON.parse(responseText) if(responseData){ if(!svType||!currentCRS){ svType='baidu' currentCRS='BD09'} const latitude = responseData.data.lat const longitude =responseData.data.lng 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 (!map) createMap() if(!globalPanoId) globalPanoId=currentPanoId 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] if (globalPanoId){ const year=parseInt(globalPanoId.substring(10,12)) const month=parseInt(globalPanoId.substring(12,14)) const day=parseInt(globalPanoId.substring(14,16)) globalTimeInfo = `20${year}年${month}月${day}日`; } else{ globalTimeInfo = '未知时间'; } const heading=(responseData.data.centerHeading)-90 if (latitude && longitude) { currentLink = `https://map.baidu.com/@13057562,4799985#panoid=${globalPanoId}&panotype=street&heading=${heading}&pitch=0&l=21&tn=B_NORMAL_MAP&sc=0&newmap=1&shareurl=1&pid=${globalPanoId}`; } if (api_key){ getAddressFromGD(latitude,longitude) .then(address => { if (address) { areaButton.innerHTML=`
${address}
` } }) .catch(error => { console.error('获取地址时发生错误:', error); }); } else{ getAddressFromOSM(latitude,longitude) .then(address => { if (address) { areaButton.innerHTML=`
${processAddress(address)}
` } }) .catch(error => { console.error('获取地址时发生错误:', error); }); } if (globalPanoId){ getPano(globalPanoId) .then(pano => { if (pano) { globalStreetInfo=pano.Rname } }) .catch(error => { console.error('获取路名时发生错误:', error); }); } } } }, 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=20`; 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 { localStorage.removeItem('api_key') Swal.fire('无效的API密钥','请刷新页面并重新输入正确的高德地图API密钥','error'); reject(new Error('Request failed: ' + data.info)); } } else { 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 getPano(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]){ var Rname=data.content[0].Rname if(data.content[0].Rname==="") Rname=null resolve({X:data.content[0].X,Y:data.content[0].Y,Rname:Rname})} else{ resolve('未知道路') } } catch (error){ resolve('未知道路')} }) .catch(error => { console.error('Error fetching road name:', error); reject(error); }); }); } 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 correctCoord(lat,lng){ if (svType==='google'&¤tCRS==='BD09'){ const correct_point=gcoord.transform([lng,lat], gcoord.BD09,gcoord.WGS84).reverse() return correct_point } else if (svType==='baidu'&¤tCRS==='BD09'){ const correct_point=gcoord.transform([lng,lat], gcoord.GCJ02,gcoord.WGS84).reverse() return correct_point } else{ return [lat,lng] } } 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) pin= L.marker(correct_coord,{icon:playerIcon}).addTo(map) pins.push(pin) } else { const playerIcon=getCustomIcon('blue',guess.userIcon) pin= L.marker(correct_coord,{icon:playerIcon}).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); marker.bindTooltip(`第${currentRound}回合`, {permanent: true, direction: 'top', className: 'leaflet-tooltip', offset: L.point(0, -40), opacity: 1 }).openTooltip() if (!previousPin){ map.setView(correct_coord, 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 createMap(){ 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='250px' guideMap.style.zIndex='9999' document.body.appendChild(guideMap) 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 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 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/23.12.09.11/{z}/{x}/{y}/?language=zh&p=46&scale=2&mapType=ROADMAP&presetStyleId=standard&pattern=JPG&key=DAEDANitav6P7Q0lWzCzKkLErbrJG4kS1u%2FCpEe5ZyxW5u0nSkb40bJ%2BYAugRN03fhf0BszLS1rCrzAogRHDZkxaMrloaHPQGO6LNg==") 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.5") 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 bdRoadmapLayers = {"去除标签":basemapLayer,"街景覆盖":svLayer} const bdSatelliteLayers={"路网标注":satelliteLabelsLayer,"街景覆盖":svLayer } var gsvLayers={"街景覆盖": gsvLayer,"官方覆盖": gsvLayer2,"非官方覆盖": gsvLayer3,"地图标签":googleLabelsLayer} const baseLayers={ "百度地图": baseLabelsLayer,"百度卫星图": satelliteBaseLayer,"华为地图":hwLayer,"谷歌地图":googleLayer,"谷歌地形图":terrainLayer,"谷歌卫星图":googleSatelliteLayer,"OSM":osmLayer } var layerControl=L.control.layers(baseLayers,{ "街景覆盖": gsvLayer, "官方覆盖": gsvLayer2, "非官方覆盖": gsvLayer3 },{ autoZIndex: false, position:"bottomleft"}) var opacityControl currentCRS='WGS84' map = L.map("guide-map", {zoomControl: false, attributionControl: false, doubleClickZoom: false,preferCanvas: true}) hwLayer.addTo(map) gsvLayer.addTo(map).bringToFront gsvLayer.setOpacity(0) opacityControl=L.control.opacityControl(gsvLayer, { position: 'topright' }).addTo(map) if (guesses.length>0) { drawPins() } let timeoutId; guideMap.addEventListener('mouseenter', function() { layerControl.addTo(map); guideMap.style.width = '900px'; guideMap.style.height = '600px'; map.invalidateSize(); if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } }); guideMap.addEventListener('mouseleave', function() { timeoutId = setTimeout(function() { map.removeControl(layerControl); guideMap.style.width = '300px'; guideMap.style.height = '250px'; map.invalidateSize(); }, 700); }); 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(); if (newBaseLayer._url.includes('maprastertile') || newBaseLayer._url.includes('osm')) { map.removeLayer(googleLabelsLayer); 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() { 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(replayData); }, 500) } 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){ let index = 0; let replayPin let previousTime = events[0].time; let mapCenter pins.forEach(pin => { pin.setOpacity(0) }); function applyNextEvent() { if (index >= events.length) { pins.forEach(pin => { pin.setOpacity(1) }); return}; const event = events[index]; const delay = event.time - previousTime; switch (event.type) { case 'PanoPosition': streetViewPanorama.setPano(event.data); break; case 'PanoPov': streetViewPanorama.setPov({ heading: parseFloat(event.data[0]), pitch: parseFloat(event.data[1]) }); break; case 'PanoZoom': streetViewPanorama.setZoom(parseFloat(event.data)); break; case 'MapView': mapCenter=correctCoord(parseFloat(event.data[0]),parseFloat(event.data[1])) map.setView(mapCenter); break; case 'MapZoom': mapCenter=correctCoord(parseFloat(event.data[0]),parseFloat(event.data[1])) map.flyTo(mapCenter, event.data[2], { duration:delay/2000 }); break; case 'MapSize': if(event.data[0] { if (e.key === 'r' || e.key === 'R') { e.stopImmediatePropagation(); localStorage.removeItem('address_source') localStorage.removeItem('api_key') Swal.fire('清除成功','获取地址信息的来源已重置,您的API密钥已从缓存中清除,请刷新页面后重新选择。','success'); } if (e.key === 'm' || e.key === 'M') { e.stopImmediatePropagation(); getSvContainer() console.log(streetViewPanorama) if (isMapDisplay){ guideMap.style.display='none' isMapDisplay=false } else{ guideMap.style.display='block' isMapDisplay=true } } } document.addEventListener("keydown", onKeyDown); })();