// ==UserScript== // @name 图寻复盘工具 PRO // @namespace https://greasyfork.org/users/1179204 // @version 1.3.3 // @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); }; 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 yellowIcon = new L.Icon({ iconUrl: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-yellow.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] }); 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.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.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(svType) 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,yellowIcon) } previousPin=[latitude,longitude] } var countryCode try{ countryCode = panoData[1][0][5][0][1][4]} catch(error){ countryCode=null } if (countryCode===('HK'||'TW'||'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=`