您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
增加复盘小地图,全面提升复盘效果
当前为
// ==UserScript== // @name 图寻复盘工具 PRO // @namespace https://gf.qytechs.cn/users/1179204 // @version 1.5.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/[email protected]/dist/leaflet.js // @require https://unpkg.com/gcoord/dist/gcoord.global.prod.js // @require https://cdn.jsdelivr.net/npm/[email protected]/suncalc.min.js // ==/UserScript== (function() { 'use strict'; GM_addStyle(` @import url('https://unpkg.com/[email protected]/dist/leaflet.css'); #panels { position: fixed; top: 100px; left: 10px; padding: 10px; border-radius: 20px !important; z-index: 1000; display: flex; flex-direction: column; width: 180px; } #panels button { cursor: pointer; width: 100% !important; font-weight: bold !important; border: 8px solid #000000 !important; text-align: left !important; padding-left: 8px !important; padding-right: 8px !important; backdrop-filter: blur(10px); margin-bottom: 5px; border-radius: 4px; background-color: #000000 !important; color: #A0A0A0 !important; } #timeline { cursor: pointer; width: 100%; font-weight: bold; font-size:14px; border: 8px solid #000000; text-align: left; padding-left: 4px; padding-right: 2px; backdrop-filter: blur(10px); margin-bottom: 5px; border-radius: 4px; background-color: #000000; color: #A0A0A0; } .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 = ` <input type="range" id="opacity-slider" min="0" max="100" value="0" step="10" style="margin:5px; width:90px"> `; L.DomEvent.disableClickPropagation(container); L.DomEvent.disableScrollPropagation(container); L.DomEvent.on(container.querySelector('#opacity-slider'), 'input', function (e) { var opacity = e.target.value / 100; this._currentOpacity = opacity; this.layer.setOpacity(opacity) }.bind(this)); return container; } }); L.control.opacityControl = function(opts) { return new L.Control.OpacityControl(opts); }; function getCustomIcon(color, url) { if (!url) url="https://i.chao-fan.com/f58b7f52d7c801ba0806e2125a776a44.png" return L.divIcon({ className: 'custom-icon', html: ` <div class="marker-background" style="height:100%;width:100%; background-image: url("https://s.chao-fan.com/tuxun/images/marker_background_${color}.png"); background-size: 100%; background-repeat: no-repeat; overflow:hidden;"> <img src="https://i.chao-fan.com/${url}?x-oss-process=image/resize,h_80/quality,q_100" style="position: absolute; top: 38%; left: 50%; width:28px; height:28px; transform: translate(-50%, -50%); border-radius: 100%" /> </div> `, iconSize: [30, 42], iconAnchor: [15, 42], popupAnchor: [1, -34], shadowSize: [42, 42] }); } const flagIcon = new L.divIcon({ className: 'custom-icon', html: ` <div class="marker-background" style="height:100%;width:100%; background-image: url("https://s.chao-fan.com/tuxun/images/marker_background_black.png"); background-size: 100%; background-repeat: no-repeat;"> <span role="img" aria-label="flag" class="anticon anticon-flag" style="position:absolute; font-size: 20px; left:24%; top:16%"><svg viewBox="64 64 896 896" focusable="false" data-icon="flag" width="1em" height="1em" fill="currentColor" aria-hidden="true" style="transform: rotate(-45deg);"><path d="M184 232h368v336H184z" fill="#404040"></path><path d="M624 632c0 4.4-3.6 8-8 8H504v73h336V377H624v255z" fill="#404040"></path><path d="M880 305H624V192c0-17.7-14.3-32-32-32H184v-40c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v784c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V640h248v113c0 17.7 14.3 32 32 32h416c17.7 0 32-14.3 32-32V337c0-17.7-14.3-32-32-32zM184 568V232h368v336H184zm656 145H504v-73h112c4.4 0 8-3.6 8-8V377h216v336z" fill="warning"></path></svg></span> </div> `, iconSize: [36, 44], iconAnchor: [18, 44], popupAnchor: [1, -34], }); let guideMap,map,service,marker,pins=[],pathCoords=[],paths=[],svType,previousPin,currentCRS,startPoint,streetViewPanorama,isMapDisplay=true,isJump=false const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun','Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 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', backdrop: null, text: 'OSM具有更详细的地址信息,高德地图的获取速度更快且带有电话区号信息(需要自行注册(不可用)API密钥)', showCancelButton: true, allowOutsideClick: false, confirmButtonColor: '#3085d6', confirmButtonText: 'OSM', cancelButtonText: '高德地图', }).then((result) => { if (result.isConfirmed) { localStorage.setItem('address_source', JSON.stringify('OSM')); address_source='OSM' } else if (result.dismiss === Swal.DismissReason.cancel) { localStorage.setItem('address_source', JSON.stringify('GD')); address_source=JSON.parse(localStorage.getItem('address_source')) Swal.fire({ title: '请输入您的高德地图 API 密钥', input: 'text', inputPlaceholder: '', showCancelButton: true, backdrop: null, confirmButtonText: '保存', cancelButtonText: '取消', preConfirm: (inputValue) => { if (inputValue.length===32){ return inputValue; } else{ Swal.showValidationMessage('请输入有效的高德地图API密钥!') } } }).then((result) => { if (result.isConfirmed) { if(result.value){ localStorage.setItem('api_key', JSON.stringify(result.value)); Swal.fire('保存成功!', '您的API密钥已保存,请刷新页面。', 'success');} else{ localStorage.removeItem('address_source') } } }); } }); } if(!api_key&&address_source==='GD'){ Swal.fire({ title: '请输入您的高德地图 API 密钥', input: 'text', inputPlaceholder: '', backdrop: null, showCancelButton: true, confirmButtonText: '保存', cancelButtonText: '取消', preConfirm: (inputValue) => { if (inputValue.length===32){ return inputValue; } else{ Swal.showValidationMessage('请输入有效的高德地图API密钥!') } } }).then((result) => { if (result.isConfirmed) { if(result.value){ api_key=JSON.parse(localStorage.getItem('api_key')); Swal.fire('保存成功!', '您的API密钥已保存,请刷新页面。', 'success');} } else{ localStorage.removeItem('address_source') } }); } 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 = '地区'; container.appendChild(areaButton); const streetButton = document.createElement('button'); streetButton.textContent = '路名'; container.appendChild(streetButton); const altitudeButton = document.createElement('button'); altitudeButton.textContent = '海拔'; container.appendChild(altitudeButton); const downloadButton=document.createElement('button') downloadButton.textContent = '下载全景'; container.appendChild(downloadButton); downloadButton.onclick =async () =>{ const { value: zoom, dismiss: inputDismiss } = await Swal.fire({ title: '请选择下载的图像质量等级\n(腾讯和百度无法选择)', html:'<select id="zoom-select" class="swal2-input" style="width:180px; height:40px; font-size:16px;white-space:prewrap">' + '<option value="1">高糊 (100KB~500KB)</option>' + '<option value="2">模糊 (500KB~1MB)</option>' + '<option value="3">标准 (1MB~4MB)</option>' + '<option value="4">高清 (4MB~8MB)</option>' + '<option value="5">原画 (8MB~15MB)</option>' + '</select>', icon: 'question', showCancelButton: true, showCloseButton: true, allowOutsideClick: false, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: 'Yes', cancelButtonText: 'Cancel', backdrop: null, preConfirm: () => { return document.getElementById('zoom-select').value; } }); if (zoom){ const currentUrl = window.location.href; const fileName = `${globalPanoId}.jpg`; if(svType=='google'){ const metaData = await searchGooglePano('GetMetadata', globalPanoId); var w=metaData.worldWidth var h=metaData.worldHeight } const swal = Swal.fire({ title: '下载中', text: '请稍候', allowOutsideClick: false, allowEscapeKey: false, showConfirmButton: false, backdrop: null, didOpen: () => { Swal.showLoading(); } }); await downloadPanoramaImage(globalPanoId, fileName,w,h,parseInt(zoom)); swal.close() Swal.fire({ title: '下载完成!', text: '全景图片已保存到你的电脑', icon: 'success', backdrop: false }); } } const timeline = document.createElement('select'); timeline.id='timeline' container.appendChild(timeline); timeline.addEventListener('change', function() { if(!streetViewPanorama)getSvContainer() streetViewPanorama.setPano(timeline.value); }); const panoIdButton = document.createElement('button'); panoIdButton.textContent = '全景Id'; container.appendChild(panoIdButton); panoIdButton.onclick =async () => { if(!streetViewPanorama)getSvContainer() globalPanoId=streetViewPanorama.pano GM_setClipboard(globalPanoId, 'text'); panoIdButton.textContent='复制成功!' setTimeout(function() { panoIdButton.textContent=globalPanoId&&svType=='baidu' ? `${globalPanoId.substring(6,10)}, ${globalPanoId.substring(25,27)}` : 'panoId' }, 1000) }; 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 globalLat,globalLng,globalTimestamp 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 if (svType==='qq') return currentLink else shortUrl=await getBDSL(globalPanoId,POV.heading,POV.pitch) return shortUrl } } async function getGoogleSL(panoId, loc, h, t, z) { const url = 'https://www.google.com/maps/rpc/shorturl'; const y=calculateFOV(z) const pb = `!1shttps://www.google.com/maps/@${loc.lat()},${loc.lng()},3a,${y}y,${h}h,${t+90}t/data=*213m7*211e1*213m5*211s${panoId}*212e0*216shttps%3A%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fpanoid%3D${panoId}%26cb_client%3Dmaps_sv.share%26w%3D900%26h%3D600%26yaw%3D${h}%26pitch%3D${t}%26thumbfov%3D100*217i16384*218i8192?coh=205410&entry=tts&g_ep=EgoyMDI0MDgyOC4wKgBIAVAD!2m2!1sH5TSZpaqObbBvr0PvKOJ0AI!7e81!6b1`; const params = new URLSearchParams({ authuser: '0', hl: 'en', gl: 'us', pb: pb }).toString(); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `${url}?${params}`, onload: function(response) { if (response.status >= 200 && response.status < 300) { try { const text = response.responseText; const match = text.match(/"([^"]+)"/); if (match && match[1]) { resolve(match[1]); } else { reject('No URL found.'); } } catch (error) { reject('Failed to parse response: ' + error); } } else { reject('Request failed with status: ' + response.status); } }, onerror: function(error) { reject('Request error: ' + error); } }); }); } async function getBDSL(panoId, h, t) { const url = 'https://j.map.baidu.com/?'; const target = `https://map.baidu.com/?newmap=1&shareurl=1&panoid=${panoId}&panotype=street&heading=${h}&pitch=${t}&l=21&tn=B_NORMAL_MAP&sc=0&newmap=1&shareurl=1&pid=${panoId}`; const params = new URLSearchParams({ url: target, web: 'true', pcevaname: 'pc4.1', newfrom:'zhuzhan_webmap', callback:'jsonp94641768' }).toString() return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `${url}${params}`, onload: function(response) { if (response.status >= 200 && response.status < 300) { try { const data = response.responseText; const urlRegex = /\((\{.*?\})\)$/; const match = data.match(urlRegex); if (match && match[1]) { const jsonData = JSON.parse(match[1].replace(/\\\//g, '/')); resolve(jsonData.url) } else { console.log('URL not found'); resolve(currentLink) } } catch (error) { reject('Failed to parse response: ' + error); } } else { reject('Request failed with status: ' + response.status); } }, onerror: function(error) { reject('Request error: ' + error); } }); }); } function calculateFOV(zoom) { const pi = Math.PI; const argument = (3 / 4) * Math.pow(2, 1 - zoom); const radians = Math.atan(argument); const degrees = (360 / pi) * radians; return degrees; } function updateButtonContent() { streetButton.textContent = globalStreetInfo ? `${globalStreetInfo}` : '未知道路'; } setInterval(updateButtonContent, 500); function getSvContainer(){ const streetViewContainer= document.getElementById('viewer') const keys = Object.keys(streetViewContainer) const key = keys.find(key => key.startsWith("__reactFiber")) const props = streetViewContainer[key] streetViewPanorama=props.return.return.memoizedState.baseState } function createPanoSelector(panoData,selector) { selector.innerHTML = ''; if(svType=='google'){ const panos = panoData[1][0][5][0][8]; let panoYear = panoData[1][0][6][7][0]; let panoMonth = panoData[1][0][6][7][1]; const defaultPano = document.createElement('option'); defaultPano.value = globalPanoId; defaultPano.textContent = `${panoYear}年${panoMonth}月`; selector.appendChild(defaultPano); if (panos&&panos.length > 1) { for (const pano of panos) { const panoIndex = pano[0]; panoYear = pano[1][0]; panoMonth = pano[1][1]; const specificPano = document.createElement("option"); specificPano.value = panoData[1][0][5][0][3][0][panoIndex][0][1]; specificPano.textContent = `${panoYear}年${panoMonth}月`; selector.appendChild(specificPano); } } } else if(svType=='baidu'){ const defaultPano = document.createElement('option'); defaultPano.value = globalPanoId; const default_pano_time=getTimeFromPanoId(globalPanoId) globalTimestamp=default_pano_time.timestamp defaultPano.textContent = default_pano_time.timeInfo; selector.appendChild(defaultPano); for (const pano of panoData) { if(pano.ID!=globalPanoId){ const specificPano = document.createElement("option"); const pano_time=getTimeFromPanoId(pano.ID) specificPano.value = pano.ID; specificPano.textContent = pano_time.timeInfo; selector.appendChild(specificPano);} } } else{ const defaultPano = document.createElement('option'); defaultPano.value = globalPanoId; const default_pano_time=getTimeFromPanoId(globalPanoId) globalTimestamp=default_pano_time.timestamp defaultPano.textContent = default_pano_time.timeInfo; selector.appendChild(defaultPano); try{ for (const pano of panoData) { if(pano.svid!=globalPanoId){ const specificPano = document.createElement("option"); const pano_time=getTimeFromPanoId(pano.svid) specificPano.value = pano.svid; specificPano.textContent = pano_time.timeInfo; selector.appendChild(specificPano);} } } catch(e){ console.log("Faile to set timeline: "+e) } } } function parseRoundData(data, targetRound) { const result = []; data.forEach(team => { team.teamUsers.forEach(user => { user.guesses.forEach(guess=>{ if (targetRound===guess.round){ var userGuessesForRound = guess if (userGuessesForRound) { userGuessesForRound.userName=user.user.userName userGuessesForRound.userId=user.user.userId 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('eId=')) { 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' } const responseText = this.responseText; const panoData=JSON.parse(responseText) createPanoSelector(panoData, timeline); 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) { globalLat = coordinateMatches[2] globalLng = coordinateMatches[3] if (!map) createMap() if(!streetViewPanorama) getSvContainer() const currentPanoId=streetViewPanorama.getPano() if(!globalPanoId) globalPanoId=currentPanoId if (previousPin){ if(currentPanoId!=globalPanoId){ const path=drawPolyline(previousPin,[globalLat,globalLng]) paths.push(path) pathCoords.push([previousPin,[globalLat,globalLng]]) globalPanoId=currentPanoId} } else{ startPoint=[globalLat,globalLng] addMarker(globalLat,globalLng,flagIcon) } previousPin=[globalLat,globalLng] } var countryCode try{ countryCode = 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=` <div class="stat-value">${countryCode? `<img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">` : ''}${countryCode}</div>` } if (areaMatches) { areaButton.innerHTML=` <div class="stat-value">${countryCode? `<img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">` : ''}${countryCode},${areaMatches[0]}</div>` } if(countryCode=='IN'){ if(globalLat>=26.5&&globalLng>=91){ areaButton.style.display='none' streetButton.style.display='none' } } var addressMatches try{ addressMatches = panoData[1][0][3][2][0][0]} catch(error){ addressMatches=null } if (addressMatches) { globalStreetInfo = addressMatches; } else { globalStreetInfo = '未知地址'; } } if (this._url && this._url.includes('getPanoInfo')) { const flag = `https://flagicons.lipis.dev/flags/4x3/cn.svg`; 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 globalLat=latitude globalLng=longitude const mars_point=gcoord.transform([longitude,latitude], gcoord.GCJ02,gcoord.WGS84).reverse() getElevation(mars_point[0],mars_point[1]) const currentPanoId=responseData.data.pano if (!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] 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=` <div class="stat-value"><img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">${address}</div>` } }) .catch(error => { console.error('获取地址时发生错误:', error); }); } else{ getAddressFromOSM(latitude,longitude) .then(address => { if (address) { areaButton.innerHTML=` <div class="stat-value"><img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">${processAddress(address)}</div>` } }) .catch(error => { console.error('获取地址时发生错误:', error); }); } if (globalPanoId){ getBDPano(globalPanoId) .then(pano => { if (pano) { globalStreetInfo=pano.Rname createPanoSelector(pano.timeline,timeline) } }) .catch(error => { console.error('获取街景数据失败:', error); }); } } } if (this._url && this._url.includes('getQQPanoInfo')) { 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='qq' currentCRS='WGS84' } const latitude = responseData.data.lat const longitude =responseData.data.lng globalLat=latitude globalLng=longitude getElevation(latitude,longitude) const currentPanoId=responseData.data.pano if (currentPanoId) { currentLink=`https://qq-map.netlify.app/#base=roadmap&zoom=4¢er=${latitude}%2C${longitude}&pano=${currentPanoId}` } 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] const heading=(responseData.data.centerHeading)-90 if (api_key){ getAddressFromGD(latitude,longitude) .then(address => { if (address) { areaButton.innerHTML=` <div class="stat-value"><img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">${address}</div>` } }) .catch(error => { console.error('获取地址时发生错误:', error); }); } else{ getAddressFromOSM(latitude,longitude) .then(address => { if (address) { areaButton.innerHTML=` <div class="stat-value"><img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">${processAddress(address)}</div>` } }) .catch(error => { console.error('获取地址时发生错误:', error); }); } if (globalPanoId){ getQQPano(globalPanoId) .then(pano => { if (pano) { globalStreetInfo=pano.Rname createPanoSelector(pano.timeline,timeline) } }) .catch(error => { console.error("获取街景失败:", error); }); } } } panoIdButton.textContent=globalPanoId&&svType=='baidu' ? `${globalPanoId.substring(6,10)}, ${globalPanoId.substring(25,27)}` : 'panoId' }, false); realSend.call(this, value); function getAddressFromGD(lat, lng) { return new Promise((resolve, reject) => { const apiUrl = `https://restapi.amap.com/v3/geocode/regeo?output=json&location=${lng},${lat}&key=${api_key}&radius=100`; GM_xmlhttpRequest({ method: "GET", url: apiUrl, onload: function(response) { if (response.status === 200) { const data = JSON.parse(response.responseText); if (data.status === '1' && data.regeocode) { const province=data.regeocode.addressComponent.province const city=data.regeocode.addressComponent.city const district=data.regeocode.addressComponent.district const township=data.regeocode.addressComponent.township const cityCode=data.regeocode.addressComponent.citycode const addressInfo={province,city,district,township,cityCode} var formatted_address= '中国' for (const key in addressInfo) { if (addressInfo[key]) { if (addressInfo[key]!='') { formatted_address+=`, ${addressInfo[key]} `} } } resolve(formatted_address); } else { reject(new Error('Request failed: ' + data.info)); } } else { localStorage.removeItem('api_key') Swal.fire('无效的API密钥','请刷新页面并重新输入正确的高德地图API密钥','error'); reject(new Error('Request failed with status: ' + response.status)); } }, onerror: function(error) { console.error('Error fetching address:', error); reject(error); } }); });} function getAddressFromOSM(lat, lng) { return new Promise((resolve, reject) => { const apiUrl = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1&accept-language=cn`; fetch(apiUrl) .then(response => response.json()) .then(data => { if (data.display_name) resolve(data.display_name); else resolve('未知') }) .catch(error => { console.error('Error fetching address:', error); reject(error); }); }); } async function getElevation(lat, lng) { const url = `https://api.open-meteo.com/v1/elevation?latitude=${lat}&longitude=${lng}`; try { const response = await fetch(url); if (!response.ok) { console.error(`HTTP error! Status: ${response.status}`); return null } const data = await response.json(); const altitude = data.elevation; if(altitude) altitudeButton.textContent=`海拔:${altitude[0]}m` else altitudeButton.textContent=`未知海拔` } catch (error) { console.error('Error fetching elevation data:', error); return null; } } function processAddress(text) { const items = text.split(',').map(item => item.trim()); const filteredItems = items.filter(item => isNaN(item)); const reversedItems = filteredItems.reverse(); const result = reversedItems.join(', '); return result; } } function getTimeFromPanoId(panoId){ var year,month,day,hour,min,timeInfo if (panoId){ if(svType=='baidu'){ year = parseInt(panoId.substring(10, 12)); month = parseInt(panoId.substring(12, 14)) - 1; day = parseInt(panoId.substring(14, 16)); hour = parseInt(panoId.substring(16, 18)); min = parseInt(panoId.substring(18, 20));} else{ year = parseInt(panoId.substring(8, 10)); month = parseInt(panoId.substring(10, 12)) - 1; day = parseInt(panoId.substring(12, 14)); hour = parseInt(panoId.substring(14, 16)); min = parseInt(panoId.substring(16, 18)); } const date = new Date(2000 + year, month, day, hour, min); if (parseInt(hour) >= 19) { timeInfo = `20${year}年${month + 1}月${day}日🌙`; } else { timeInfo = `20${year}年${month + 1}月${day}日🌞`; } return {timeInfo:timeInfo,timestamp:date.getTime()} } } function getBDPano(id){ return new Promise((resolve, reject) => { const url = `https://mapsv0.bdimg.com/?qt=sdata&sid=${id}`; fetch(url) .then(response => response.json()) .then(data => { try{ if(data.content[0]){ const meta=data.content[0] var Rname=meta.Rname if(Rname==="") Rname=null resolve({X:meta.X,Y:meta.Y,Rname:Rname,timeline:meta.TimeLine})} else{ resolve('获取街景元数据失败') } } catch (error){ resolve('请求失败',error)} }) .catch(error => { console.error('Error fetching pano data:', error); reject(error); }); }); } function getQQPano(id) { return new Promise((resolve, reject) => { const url = `https://sv.map.qq.com/sv?svid=${id}&output=json`; fetch(url, { method: 'GET' }) .then(function (resp){ return resp.blob() }) .then(function (body) { var reader= new FileReader() reader.onload=function(e){ var text =reader.result const data=JSON.parse(text) if (data.detail) { var metadata = data.detail.basic; if (metadata) { var Rname = metadata.append_addr; var heading=parseFloat(metadata.dir) var trans=metadata.trans_svid var history={} if(data.detail.history&&data.detail.history.nodes)history=data.detail.history.nodes if(trans!='') history.push({svid:trans}) resolve({ X: metadata.x, Y: metadata.y, Rname: Rname, heading:heading, timeline:history||null }); } } else { resolve('获取元数据失败'); } } reader.readAsText(body,'GBK') }); }) } async function searchQQPano(lat,lng,zoom) { const r=(21-zoom)*500 return new Promise((resolve, reject) => { const url = `https://sv.map.qq.com/xf?lat=${lat}&lng=${lng}&r=${r}&output=jsonv`; fetch(url) .then(response => response.json()) .then(data => { const pano=data.detail if(pano.svid!='')resolve({heading:pano.heading,panoId:pano.svid}) else resolve(null) }) .catch(error => { console.error('获取腾讯街景失败:', error); resolve(null) }); }); } async function searchGooglePano(t, e, z) { try { const u = `https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/${t}`; const r=50*(21-z)**2 let payload = createPayload(t,e,r); const response = await fetch(u, { method: "POST", headers: { "content-type": "application/json+protobuf", "x-user-agent": "grpc-web-javascript/0.1" }, body: payload, mode: "cors", credentials: "omit" }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { const data = await response.json(); if(t=='GetMetadata'){ return { panoId: data[1][0][1][1], heading: data[1][0][5][0][1][2][0], worldHeight:data[1][0][2][2][0], worldWidth:data[1][0][2][2][1] }; } return { panoId: data[1][1][1], heading: data[1][5][0][1][2][0] }; } } catch (error) { console.error(`获取谷歌街景失败: ${error.message}`); } } function createPayload(mode,coorData,r) { let payload; if(!r)r=50 if (mode === 'GetMetadata') { payload = [["apiv3",null,null,null,"US",null,null,null,null,null,[[0]]],["en","US"],[[[2,coorData]]],[[1,2,3,4,8,6]]]; } else if (mode === 'SingleImageSearch') { payload =[["apiv3"], [[null,null,coorData.lat,coorData.lng],r], [null,["en","US"],null,null,null,null,null,null,[2],null,[[[2,true,2],[10,true,2]]]], [[1,2,3,4,8,6]]] } else { throw new Error("Invalid mode!"); } return JSON.stringify(payload); } async function searchBDPano(lat,lng,l){ var mc if(currentCRS!='BD09') mc=gcoord.transform([lng,lat], gcoord.GCJ02,gcoord.BD09MC).reverse() else mc=gcoord.transform([lng,lat], gcoord.WGS84,gcoord.BD09MC).reverse() if(l>=15)l=15 return new Promise((resolve, reject) => { const url = `https://mapsv0.bdimg.com/?qt=qsdata&x=${mc[1]}&y=${mc[0]}&l=${l}`; fetch(url) .then(response => response.json()) .then(data => { const pano=data.content resolve({heading:0,panoId:pano.id}) }) .catch(error => { console.error('获取百度街景失败:', error); resolve(null) }); }); } function correctCoord(lat,lng){ if (svType==='google'&¤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); if(!isJump){ marker.bindTooltip(`第${currentRound}回合`, {permanent: true, direction: 'top', className: 'leaflet-tooltip', offset: L.point(0, -40), opacity: 1 }).openTooltip()} if (!previousPin&&!isJump){ map.setView(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 getSVData(service, options) { return new Promise(resolve => service.getPanorama({...options}, (data, status) => { resolve(data); })); } 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 googleLayer = L.tileLayer("https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i{z}!2i{x}!3i{y}!4i256!2m1!2sm!3m17!2sen!3sUS!5e18!12m4!1e68!2m2!1sset!2sRoadmap!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2ss.e:l|p.v:off,s.t:1|s.e:g.s|p.v:on!5m1!5f1.5"); const googleLabelsLayer=L.tileLayer("https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i{z}!2i{x}!3i{y}!4i256!2m1!2sm!3m17!2sen!3sUS!5e18!12m4!1e68!2m2!1sset!2sRoadmap!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2ss.e:g|p.v:off,s.t:1|s.e:g.s|p.v:on,s.e:l|p.v:on!5m1!5f1.8") const gsvLayer = L.tileLayer("https://www.google.com/maps/vt?pb=!1m7!8m6!1m3!1i{z}!2i{x}!3i{y}!2i9!3x1!2m8!1e2!2ssvv!4m2!1scc!2s*211m3*211e2*212b1*213e2*211m3*211e3*212b1*213e2*212b1*214b1!4m2!1ssvl!2s*211b0*212b1!3m8!2sen!3sus!5e1105!12m4!1e68!2m2!1sset!2sRoadmap!4e0!5m4!1e0!8m2!1e1!1e1!6m6!1e12!2i2!11e0!39b0!44e0!50e0"); const gsvLayer2 = L.tileLayer("https://www.google.com/maps/vt?pb=!1m7!8m6!1m3!1i{z}!2i{x}!3i{y}!2i9!3x1!2m8!1e2!2ssvv!4m2!1scc!2s*211m3*211e2*212b1*213e2*212b1*214b1!4m2!1ssvl!2s*211b0*212b1!3m8!2sen!3sus!5e1105!12m4!1e68!2m2!1sset!2sRoadmap!4e0!5m4!1e0!8m2!1e1!1e1!6m6!1e12!2i2!11e0!39b0!44e0!50e0"); const gsvLayer3 = L.tileLayer("https://www.google.com/maps/vt?pb=!1m7!8m6!1m3!1i{z}!2i{x}!3i{y}!2i9!3x1!2m8!1e2!2ssvv!4m2!1scc!2s*211m3*211e3*212b1*213e2*212b1*214b1!4m2!1ssvl!2s*211b0*212b1!3m8!2sen!3sus!5e1105!12m4!1e68!2m2!1sset!2sRoadmap!4e0!5m4!1e0!8m2!1e1!1e1!6m6!1e12!2i2!11e0!39b0!44e0!50e0"); const googleSatelliteLayer = L.tileLayer("https://www.google.com/maps/vt?pb=!1m7!8m6!1m3!1i{z}!2i{x}!3i{y}!2i9!3x1!2m2!1e1!2sm!3m3!2sen!3sus!5e1105!4e0!5m4!1e0!8m2!1e1!1e1!6m6!1e12!2i2!11e0!39b0!44e0!50e0"); const googleRoadnLabelsLayer=L.tileLayer("https://mts.googleapis.com/vt?hl=zh-CN&lyrs=h&style=&x={x}&y={y}&z={z}") const terrainLayer = L.tileLayer("https://www.google.com/maps/vt?pb=!1m5!1m4!1i{z}!2i{x}!3i{y}!4i256!2m1!2sm!2m2!1e5!2sshading!2m2!1e6!2scontours!3m17!2sen!3sUS!5e18!12m4!1e68!2m2!1sset!2sTerrain!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2ss.e:l|p.v:off,s.t:0.8|s.e:g.s|p.v:on!5m1!5f1.5"); const hwLayer=L.tileLayer("https://maprastertile-drcn.dbankcdn.cn/display-service/v1/online-render/getTile/23.12.09.11/{z}/{x}/{y}/?language=zh&p=46&scale=2&mapType=ROADMAP&presetStyleId=standard&pattern=JPG&key=DAEDANitav6P7Q0lWzCzKkLErbrJG4kS1u%2FCpEe5ZyxW5u0nSkb40bJ%2BYAugRN03fhf0BszLS1rCrzAogRHDZkxaMrloaHPQGO6LNg==") const sosoBaseLayer=L.tileLayer("http://rt{s}.map.gtimg.com/realtimerender?z={z}&x={x}&y={-y}&type=vector", { subdomains: ["0","1", "2", "3"] }) const St = L.TileLayer.extend({ initialize: function (options) { L.setOptions(this, options); this._url = 'https://p1.map.gtimg.com/demTiles' }, getTileUrl: function (coords) { const { x, y, z } = coords; const flippedY = Math.pow(2, z) - 1 - y; const tileX = Math.floor(x / 16); const tileY = Math.floor(flippedY / 16); const subdomain = ["0", "1", "2", "3"]; const subdomainIndex = Math.floor(Math.random() * subdomain.length); const subdomainValue = subdomain[subdomainIndex]; return `https://p${subdomainValue}.map.gtimg.com/demTiles/${z}/${tileX}/${tileY}/${x}_${flippedY}.jpg`; } }); const sosoTerrainLayer = new St({ subdomains: ["0", "1", "2", "3"], tileSize: 256, maxZoom: 20, }); //const qsvLayer=L.tileLayer("https://mapvectors.map.qq.com/mobile_street?df=1&idx={x}&lv={z}&dth=20&bn=1&bl={y}&type=vector") const bdRoadmapLayers = {"去除标签":basemapLayer,"街景覆盖":svLayer} const bdSatelliteLayers={"路网标注":satelliteLabelsLayer,"街景覆盖":svLayer } var gsvLayers={"街景覆盖": gsvLayer,"官方覆盖": gsvLayer2,"非官方覆盖": gsvLayer3,"地图标签":googleLabelsLayer} const baseLayers={ "百度地图": baseLabelsLayer,"百度卫星图": satelliteBaseLayer,"华为地图":hwLayer,"腾讯地图":sosoBaseLayer,"腾讯地形图":sosoTerrainLayer,"谷歌地图":googleLayer,"谷歌地形图":terrainLayer,"谷歌卫星图":googleSatelliteLayer,"OSM":osmLayer } map = L.map("guide-map", {zoomControl: false, attributionControl: false, doubleClickZoom: false,preferCanvas: true}) var layerControl,opacityControl currentCRS='WGS84' layerControl=L.control.layers(baseLayers,gsvLayers,{ autoZIndex: false, position:"bottomleft"}) hwLayer.addTo(map) gsvLayer.addTo(map).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('click', async (e) => { if(!service) service=new google.maps.StreetViewService() const lat = e.latlng.lat; const lng = e.latlng.lng; const zoom = map.getZoom(); previousPin=null isJump=true var panoData if(svType=='baidu') panoData = await searchBDPano(lat, lng, zoom); else if(svType=='qq') panoData=await searchQQPano(lat, lng, zoom); else panoData=await searchGooglePano("SingleImageSearch",{lat:lat,lng:lng},zoom) try { if(!streetViewPanorama)getSvContainer() if(panoData.panoId.length==44)panoData.panoId=b64Enode(panoData.panoId) streetViewPanorama.setPano(panoData.panoId) globalPanoId=streetViewPanorama.pano streetViewPanorama.setPov({heading:180-panoData.heading,pitch:0}) } catch(error) { popupOnMap(lat,lng) console.error(`未能获取该位置街景: ${error}`); } }); map.on('baselayerchange', function (event) { map.removeLayer(marker) paths.forEach(p => { map.removeLayer(p); }); paths=[] removePins() var newBaseLayer = event.layer; if (newBaseLayer instanceof L.TileLayer&&newBaseLayer._url) { if (newBaseLayer._url.includes('starpic') || newBaseLayer._url.includes('bdimg')) { if (map.options.crs != L.CRS.Baidu) { const currentCenter=map.getCenter() const currentZoom=map.getZoom() map.removeLayer(googleLabelsLayer); map.removeLayer(gsvLayer); map.options.crs = L.CRS.Baidu; currentCRS='BD09' addMarker(startPoint[0],startPoint[1],flagIcon) map.setView(currentCenter, currentZoom+1); map.removeControl(opacityControl) opacityControl=L.control.opacityControl(svLayer, { position: 'topright' }).addTo(map); svLayer.setOpacity(0) } map.removeControl(layerControl); layerControl = L.control.layers( baseLayers, newBaseLayer._url.includes('starpic') ? bdSatelliteLayers : bdRoadmapLayers, { autoZIndex: false, position: "bottomleft" } ).addTo(map); svLayer.addTo(map).bringToFront(); } else { if (map.options.crs === L.CRS.Baidu) { const currentCenter=map.getCenter() const currentZoom=map.getZoom() map.removeLayer(svLayer); map.options.crs = L.CRS.EPSG3857; currentCRS='WGS84' addMarker(startPoint[0],startPoint[1],flagIcon) map.setView(currentCenter, currentZoom-1); map.removeControl(opacityControl) opacityControl=L.control.opacityControl(gsvLayer, { position: 'topright' }).addTo(map); gsvLayer.setOpacity(0) } map.removeControl(layerControl); layerControl = L.control.layers(baseLayers, gsvLayers, { autoZIndex: false, position: "bottomleft" }); gsvLayer.addTo(map).bringToFront() googleLabelsLayer.addTo(map).bringToFront() if (newBaseLayer._url.includes('maprastertile') || newBaseLayer._url.includes('osm')||newBaseLayer._url.includes('gtimg')) { map.removeLayer(googleLabelsLayer); if (newBaseLayer._url.includes('demTiles')){ layerControl = L.control.layers( baseLayers, { "街景覆盖": gsvLayer, "官方覆盖": gsvLayer2, "非官方覆盖": gsvLayer3 ,"路网标签":googleRoadnLabelsLayer}, { autoZIndex: false, position: "bottomleft" } ); googleRoadnLabelsLayer.addTo(map).bringToFront() } else{ map.removeLayer(googleRoadnLabelsLayer) 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 popupOnMap(lat, lng) { const popup = L.tooltip() .setLatLng([lat, lng]) .setContent('无法获取该位置的街景!') .openOn(map); setTimeout(() => { map.closePopup(popup); }, 1000); } function showRipple(lat, lng) { const latlngToPoint = map.latLngToContainerPoint([lat, lng]); const ripple = document.createElement('div'); ripple.className = 'ripple'; ripple.style.width = ripple.style.height = '50px'; ripple.style.left = `${latlngToPoint.x - 25}px`; ripple.style.top = `${latlngToPoint.y - 25}px`; ripple.style.backgroundColor = getRandomColor() ripple.style.opacity=0.7 ripple.style.zIndex='9999' guideMap.appendChild(ripple); setTimeout(() => { ripple.remove(); }, 1500); } function getRandomColor() { const r = Math.floor(Math.random() * 256); const g = Math.floor(Math.random() * 256); const b = Math.floor(Math.random() * 256); return `rgb(${r}, ${g}, ${b})`; } function createTimer(timeText) { const [minutes, seconds] = timeText.split(':').map(Number); const totalSeconds = (minutes * 60) + seconds; const container = document.createElement('div'); container.id = 'countdownContainer'; container.style.position='absolute' container.style.width = '120px'; container.style.height = '40px'; container.style.top='20px' container.style.left='50%' container.style.backgroundColor='#000000' container.style.borderRadius='21px' const timerDisplay = document.createElement('div'); timerDisplay.className = 'countdownTimer'; timerDisplay.style.position = 'absolute'; timerDisplay.style.top = '50%'; timerDisplay.style.left = '50%'; timerDisplay.style.transform = 'translate(-50%, -50%)'; timerDisplay.style.fontSize = '24px'; timerDisplay.style.fontFamily = 'Arial, sans-serif'; container.appendChild(timerDisplay); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('class', 'countdownSvg') svg.setAttribute('width', '100%'); svg.setAttribute('height', '100%'); svg.setAttribute('viewBox', '0 0 200 80'); svg.setAttribute('preserveAspectRatio', 'none'); container.appendChild(svg); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); svg.setAttribute('class','countdownPath') path.setAttribute('fill', 'rgba(0,0,0,0)'); path.setAttribute('stroke', '#FF9427'); path.setAttribute('stroke-width', '8'); path.setAttribute('d', 'M38.56,4C19.55,4,4,20.2,4,40c0,19.8,15.55,36,34.56,36h122.88C180.45,76,196,59.8,196,40c0-19.8-15.55-36-34.56-36H38.56z'); svg.appendChild(path); document.body.appendChild(container); const totalLength = path.getTotalLength(); path.style.strokeDasharray = totalLength; path.style.strokeDashoffset = totalLength; const endTime = new Date().getTime() + totalSeconds * 1000; function updateTimer() { const now = new Date().getTime(); const remainingTime = Math.max(endTime - now, 0); const remainingSeconds = Math.floor(remainingTime / 1000); const remainingMinutes = Math.floor(remainingSeconds / 60); const seconds = remainingSeconds % 60; timerDisplay.textContent = `${String(remainingMinutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; const progress = (remainingTime / (totalSeconds * 1000)) * totalLength; path.style.strokeDashoffset = totalLength - progress; if (remainingTime <= 0) { clearInterval(intervalId); timerDisplay.textContent = '00:00'; path.style.strokeDashoffset = 0; } } const intervalId = setInterval(updateTimer, 1000); updateTimer(); } function startReplay(events){ 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]<window.innerWidth*0.8){ guideMap.style.width=`${event.data[0]}px` guideMap.style.height=`${event.data[1]}px` map.invalidateSize()} break; case 'Pin': var pin=correctCoord(parseFloat(event.data[0]),parseFloat(event.data[1])) showRipple(pin[0],pin[1]) break; case 'CountDown': createTimer(event.data) break; case 'RoundEnd': var timer=document.getElementById('countdownContainer') if (timer) timer.style.display='none' break; } previousTime = event.time; index++; setTimeout(applyNextEvent, delay); } applyNextEvent(); } function b64Enode(text) { const byteArray = new Uint8Array([0x08, 0x0A, 0x12, 0x2C]); const originPanoIdBytes = new TextEncoder().encode(text); const combinedBytes = new Uint8Array(byteArray.length + originPanoIdBytes.length); combinedBytes.set(byteArray); combinedBytes.set(originPanoIdBytes, byteArray.length); let base64Encoded = btoa(String.fromCharCode.apply(null, combinedBytes)); return base64Encoded; } async function downloadPanoramaImage(panoId, fileName, w, h, zoom) { return new Promise(async (resolve, reject) => { try { let canvas, ctx, tilesPerRow, tilesPerColumn, tileUrl, imageUrl; const tileWidth = 512; const tileHeight = 512; if (svType !== 'google') { tilesPerRow = 16; tilesPerColumn = 8; } else { let zoomTiles; imageUrl = `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${panoId}&output=tile&zoom=${zoom}&nbt=0&fover=2`; zoomTiles = [2, 4, 8, 16, 32]; tilesPerRow = Math.min(Math.ceil(w / tileWidth), zoomTiles[zoom - 1]); tilesPerColumn = Math.min(Math.ceil(h / tileHeight), zoomTiles[zoom - 1] / 2); } const canvasWidth = tilesPerRow * tileWidth; const canvasHeight = tilesPerColumn * tileHeight; canvas = document.createElement('canvas'); ctx = canvas.getContext('2d'); canvas.width = canvasWidth; canvas.height = canvasHeight; const loadTile = (x, y) => { return new Promise(async (resolveTile) => { let tile; if (svType === 'qq') { tileUrl = `https://sv4.map.qq.com/tile?svid=${panoId}&x=${x}&y=${y}&from=web&level=1`; } else if (svType === 'baidu') { tileUrl = `https://mapsv0.bdimg.com/?qt=pdata&sid=${panoId}&pos=${y}_${x}&z=5`; } else { tileUrl = `${imageUrl}&x=${x}&y=${y}`; } try { tile = await loadImage(tileUrl); ctx.drawImage(tile, x * tileWidth, y * tileHeight, tileWidth, tileHeight); resolveTile(); } catch (error) { console.error(`Error loading tile at ${x},${y}:`, error); resolveTile(); } }); }; let tilePromises = []; for (let y = 0; y < tilesPerColumn; y++) { for (let x = 0; x < tilesPerRow; x++) { tilePromises.push(loadTile(x, y)); } } await Promise.all(tilePromises); canvas.toBlob(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); resolve(); }, 'image/jpeg'); } catch (error) { Swal.fire({ title: 'Error!', text: error.toString(), icon: 'error', backdrop: false }); reject(error); } }); } async function loadImage(url) { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'Anonymous'; img.onload = () => resolve(img); img.onerror = () => reject(new Error(`Failed to load image from ${url}`)); img.src = url; }); } window.addEventListener('popstate', function(event) { const container = document.getElementById('coordinates-container'); if (container) { container.remove(); } }); XMLHttpRequest.prototype.realOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url, async, user, pass) { this._url = url; this.realOpen(method, url, async, user, pass); }; let onKeyDown =async (e) => { if (e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) { return; } if (e.key === 'r' || e.key === 'R') { e.stopImmediatePropagation(); localStorage.removeItem('address_source') localStorage.removeItem('api_key') Swal.fire('清除成功','获取地址信息的来源已重置,您的API密钥已从缓存中清除,请刷新页面后重新选择。','success'); } else if (e.key === 'm' || e.key === 'M') { e.stopImmediatePropagation(); if(!streetViewPanorama)getSvContainer() if (isMapDisplay){ guideMap.style.display='none' isMapDisplay=false } else{ guideMap.style.display='block' isMapDisplay=true } } else if(e.ctrlKey&&(e.key=='i'||e.key=='I')){ if(!streetViewPanorama)getSvContainer() const allElements = document.querySelectorAll('*'); mapButton.click() streetViewPanorama.setLinks([]) allElements.forEach(element => { if (element.id === 'panels'|| element.type === 'button'|| element.classList.contains('gm-compass') || element.classList.contains('verson___kI92b') || element.classList.contains('navigate___xl6aN') )element.style.display = 'none'; }); } else if (e.key === 'x' || e.key === 'X') { e.stopImmediatePropagation(); if(!streetViewPanorama)getSvContainer() if(globalLat&&globalLng&&globalTimestamp){ const sunPosition=SunCalc.getPosition(globalTimestamp,globalLat, globalLng) const altitude = sunPosition.altitude; const azimuth = sunPosition.azimuth; const altitudeDegrees = altitude * (180 / Math.PI); const azimuthDegrees = azimuth * (180 / Math.PI); streetViewPanorama.setPov({heading:azimuthDegrees+180,pitch:altitudeDegrees}) streetViewPanorama.setZoom(1) } } else if ((e.ctrlKey )&&(e.key === 'v' || e.key === 'V')){ navigator.clipboard.readText().then(function(text) { if(svType=='qq'&&text.length!=23)return else if(svType=='baidu'&&text.length!=27) return else if(svType=='google'&&![64,44,22].includes(text.length)) return if(text.length==44)text=b64Enode(text) previousPin=null isJump=true if(!streetViewPanorama)getSvContainer() streetViewPanorama.setPano(text) globalPanoId=streetViewPanorama.pano }).catch(function(err) { console.error('读取剪贴板失败: ', err); }); } else if (e.key === 'g' || e.key === 'G') { e.stopImmediatePropagation(); if(!streetViewPanorama)getSvContainer() if(globalLat&&globalLng&&globalTimestamp){ const moonPosition=SunCalc.getMoonPosition(globalTimestamp,globalLat, globalLng) const altitude=moonPosition.altitude const azimuth = moonPosition.azimuth; const altitudeDegrees = altitude * (180 / Math.PI); const azimuthDegrees = azimuth * (180 / Math.PI); streetViewPanorama.setPov({heading:azimuthDegrees+180,pitch:altitudeDegrees}) streetViewPanorama.setZoom(1) } } } document.addEventListener("keydown", onKeyDown); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址