// ==UserScript== // @name 图寻复盘助手 PRO // @namespace https://greasyfork.org/users/1179204 // @version 1.2.7 // @match *://tuxun.fun/replay-pano?gameId=*&round=* // @icon data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDggNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0iIzAwMDAwMCI+PGcgaWQ9IlNWR1JlcG9fYmdDYXJyaWVyIiBzdHJva2Utd2lkdGg9IjAiPjwvZz48ZyBpZD0iU1ZHUmVwb190cmFjZXJDYXJyaWVyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjwvZz48ZyBpZD0iU1ZHUmVwb19pY29uQ2FycmllciI+PHRpdGxlPjcwIEJhc2ljIGljb25zIGJ5IFhpY29ucy5jbzwvdGl0bGU+PHBhdGggZD0iTTI0LDEuMzJjLTkuOTIsMC0xOCw3LjgtMTgsMTcuMzhBMTYuODMsMTYuODMsMCwwLDAsOS41NywyOS4wOWwxMi44NCwxNi44YTIsMiwwLDAsMCwzLjE4LDBsMTIuODQtMTYuOEExNi44NCwxNi44NCwwLDAsMCw0MiwxOC43QzQyLDkuMTIsMzMuOTIsMS4zMiwyNCwxLjMyWiIgZmlsbD0iI2ZmOTQyNyI+PC9wYXRoPjxwYXRoIGQ9Ik0yNS4zNywxMi4xM2E3LDcsMCwxLDAsNS41LDUuNUE3LDcsMCwwLDAsMjUuMzcsMTIuMTNaIiBmaWxsPSIjZmZmZmZmIj48L3BhdGg+PC9nPjwvc3ZnPg== // @author KaKa // @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 // @copyright KaKa // @license BSD // @description 全方位提升复盘效果 // @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 greyIcon = new L.Icon({ iconUrl: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-grey.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')); 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 = () => window.open(currentLink, '_blank'); copyButton.onclick = () => { GM_setClipboard(currentLink, 'text'); setTimeout(function() { copyButton.textContent='复制成功!' }, 100) setTimeout(function() { copyButton.textContent='复制街景链接' }, 1600) }; 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 timeButton = document.createElement('button'); timeButton.textContent = 'Time'; 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 function updateButtonContent() { areaButton.textContent = globalAreaInfo ? `${globalAreaInfo}` : '地址'; 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, 1000); 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('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 coordinatePattern = /\[\[null,null,(-?\d+\.\d+),(-?\d+\.\d+)\],\[\d+\.\d+\],\[\d+\.\d+,\d+\.\d+,\d+\.\d+\]\]|\[\s*null,\s*null,\s*(-?\d+\.\d+),\s*(-?\d+\.\d+)\s*\]/; const coordinateMatches = coordinatePattern.exec(responseText); if (coordinateMatches) { const latitude = coordinateMatches[1] || coordinateMatches[3]; const longitude = coordinateMatches[2] || coordinateMatches[4]; 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,greyIcon) } previousPin=[latitude,longitude] if (latitude && longitude) { currentLink = `https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${latitude},${longitude}`; } } const countryPattern = /,\s*"([A-Z]{2})"\s*\],null,\[/; const countryMatches = countryPattern.exec(responseText); let countryCode = countryMatches ? countryMatches[1] : '未知国家'; const areaPattern = /\[\[\s*"([^"]+)",\s*"[a-z]{2}"\s*\],\s*\["([^"]+)",\s*"zh"\s*\]\]/; const areaMatches = areaPattern.exec(responseText); if (areaMatches && areaMatches.length >= 3) { globalAreaInfo = `${countryCode}, ${areaMatches[2]}`; } const fullAddressPattern = /\[\s*null,\s*null,\s*\[\s*\["([^"]+)",\s*"[a-z]{2}"\s*\]\]/; const addressMatches = fullAddressPattern.exec(responseText); if (addressMatches && addressMatches.length > 1) { globalStreetInfo = addressMatches[1]; } else { globalStreetInfo = '未知地址'; } const timePattern = /\[\d+,\d+,\d+,null,null,\[null,null,"launch",\[\d+\]\],null,\[(\d{4}),(\d{1,2})\]\]/; const timeMatches = timePattern.exec(responseText); if (timeMatches) { globalTimeInfo = `${timeMatches[1]}年${timeMatches[2]}月`; } else { globalTimeInfo = '未知时间'; } } if (this._url && this._url.includes('getPanoInfo')) { 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() const currentPanoId=responseData.data.pano if (!map) createMap(svType) 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,greyIcon) } 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) { globalAreaInfo=address } }) .catch(error => { console.error('获取地址时发生错误:', error); }); } else{ getAddressFromOSM(latitude,longitude) .then(address => { var province,city,region,suburb,street if (address) { globalAreaInfo=processAddress(address) } }) .catch(error => { console.error('获取地址时发生错误:', error); }); } if (globalPanoId){ getRoadName(globalPanoId) .then(roadName => { if (roadName) { globalStreetInfo=roadName } }) .catch(error => { console.error('获取路名时发生错误:', error); }); } } } }, false); realSend.call(this, value); function getAddressFromGD(latitude, longitude) { return new Promise((resolve, reject) => { const apiUrl = `https://restapi.amap.com/v3/geocode/regeo?output=json&location=${longitude},${latitude}&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(latitude, longitude) { return new Promise((resolve, reject) => { const apiUrl = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&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); }); }); } function getRoadName(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].Rname&&data.content[0].Rname!=""){ resolve(data.content[0].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(svType) const _team=guesses[0].team||guesses guesses.forEach(guess => { var pin const player=guess.userName 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){ pin= L.marker(correct_coord,{icon:redIcon}).addTo(map) pins.push(pin) } else { pin= L.marker(correct_coord,{icon:blueIcon}).addTo(map) pins.push(pin) } 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(source){ 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://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i{z}!2i{x}!3i{y}!4i256!2m8!1e2!2ssvv!4m2!1scc!2s*211m3*211e2*212b1*213e2*212b1*214b1!4m2!1ssvl!2s*212b1!3m17!2sen!3sUS!5e18!12m4!1e68!2m2!1sset!2sRoadmap!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2ss.e:g.f|p.c:#1098ad|p.w:1,s.e:g.s|p.c:#99e9f2|p.w:3!5m1!5f1.5"); 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() } guideMap.addEventListener('mouseenter', function() { layerControl.addTo(map); guideMap.style.width = '900px'; guideMap.style.height = '600px'; map.invalidateSize() }); guideMap.addEventListener('mouseleave', function() { setTimeout(function() { map.removeControl(layerControl) guideMap.style.width='300px' guideMap.style.height='250px' map.invalidateSize() }, 100) }); 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],greyIcon) 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],greyIcon) 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.8 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(); if (isMapDisplay){ guideMap.style.display='none' isMapDisplay=false } else{ guideMap.style.display='block' isMapDisplay=true } } } document.addEventListener("keydown", onKeyDown); })();