图寻复盘工具 PRO

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

2025-01-20 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

  1. // ==UserScript==
  2. // @name 图寻复盘工具 PRO
  3. // @namespace https://greasyfork.org/users/1179204
  4. // @version 1.6.6
  5. // @description 增加复盘小地图,全面提升复盘效果
  6. // @match *://tuxun.fun/replay-pano?gameId=*&round=*
  7. // @icon data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDggNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0iIzAwMDAwMCI+PGcgaWQ9IlNWR1JlcG9fYmdDYXJyaWVyIiBzdHJva2Utd2lkdGg9IjAiPjwvZz48ZyBpZD0iU1ZHUmVwb190cmFjZXJDYXJyaWVyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjwvZz48ZyBpZD0iU1ZHUmVwb19pY29uQ2FycmllciI+PHRpdGxlPjcwIEJhc2ljIGljb25zIGJ5IFhpY29ucy5jbzwvdGl0bGU+PHBhdGggZD0iTTI0LDEuMzJjLTkuOTIsMC0xOCw3LjgtMTgsMTcuMzhBMTYuODMsMTYuODMsMCwwLDAsOS41NywyOS4wOWwxMi44NCwxNi44YTIsMiwwLDAsMCwzLjE4LDBsMTIuODQtMTYuOEExNi44NCwxNi44NCwwLDAsMCw0MiwxOC43QzQyLDkuMTIsMzMuOTIsMS4zMiwyNCwxLjMyWiIgZmlsbD0iI2ZmOTQyNyI+PC9wYXRoPjxwYXRoIGQ9Ik0yNS4zNywxMi4xM2E3LDcsMCwxLDAsNS41LDUuNUE3LDcsMCwwLDAsMjUuMzcsMTIuMTNaIiBmaWxsPSIjZmZmZmZmIj48L3BhdGg+PC9nPjwvc3ZnPg==
  8. // @author KaKa
  9. // @license BSD
  10. // @grant GM_setClipboard
  11. // @grant GM_addStyle
  12. // @grant GM_xmlhttpRequest
  13. // @require https://cdn.jsdelivr.net/npm/sweetalert2@11
  14. // @require https://unpkg.com/leaflet@1.9.2/dist/leaflet.js
  15. // @require https://unpkg.com/gcoord/dist/gcoord.global.prod.js
  16. // @require https://cdn.jsdelivr.net/npm/suncalc@1.9.0/suncalc.min.js
  17. // ==/UserScript==
  18. (function() {
  19. 'use strict';
  20. GM_addStyle(`
  21.  
  22. @import url('https://unpkg.com/leaflet@1.9.2/dist/leaflet.css');
  23.  
  24. #panels {
  25. position: fixed;
  26. top: 100px;
  27. left: 10px;
  28. padding: 10px;
  29. border-radius: 20px !important;
  30. z-index: 1000;
  31. display: flex;
  32. flex-direction: column;
  33. width: 180px;
  34. }
  35.  
  36. #panels button {
  37. cursor: pointer;
  38. width: 100% !important;
  39. font-weight: bold !important;
  40. border: 8px solid #000000 !important;
  41. text-align: left !important;
  42. padding-left: 8px !important;
  43. padding-right: 8px !important;
  44. backdrop-filter: blur(10px);
  45. margin-bottom: 5px;
  46. border-radius: 4px;
  47. background-color: #000000 !important;
  48. color: #A0A0A0 !important;
  49. }
  50.  
  51. #timeline {
  52. cursor: pointer;
  53. width: 100%;
  54. font-weight: bold;
  55. font-size:14px;
  56. border: 8px solid #000000;
  57. text-align: left;
  58. padding-left: 4px;
  59. padding-right: 2px;
  60. backdrop-filter: blur(10px);
  61. margin-bottom: 5px;
  62. border-radius: 4px;
  63. background-color: #000000;
  64. color: #A0A0A0;
  65. }
  66.  
  67. #replay {
  68. cursor: pointer;
  69. width: 100%;
  70. font-weight: bold;
  71. font-size:16px;
  72. border: 8px solid #000000;
  73. text-align: left;
  74. padding-left: 4px;
  75. padding-right: 2px;
  76. backdrop-filter: blur(10px);
  77. margin-bottom: 5px;
  78. border-radius: 4px;
  79. background-color: #000000;
  80. color: #A0A0A0;
  81. }
  82.  
  83. .custom-marker {
  84. background-color: red;
  85. color: white;
  86. border-radius: 50%;
  87. width: 20px;
  88. height: 20px;
  89. text-align: center;
  90. line-height: 20px;
  91. }
  92.  
  93. .leaflet-tooltip {
  94. background: rgba(255, 255, 255, 0.8);
  95. border: 0.5px solid #ccc;
  96. border-radius: 4px;
  97. font-size: 13px;
  98. color: black;
  99. font-weight: bold;
  100. }
  101.  
  102. .ripple {
  103. position: absolute;
  104. border-radius: 50%;
  105. background: rgba(0, 0, 0, 0.3);
  106. pointer-events: none;
  107. transform: scale(0);
  108. animation: ripple-animation 1s linear;
  109. }
  110.  
  111. @keyframes ripple-animation {
  112. to {
  113. transform: scale(4);
  114. opacity: 0;}
  115. }
  116.  
  117. `);
  118.  
  119. L.Projection.BaiduMercator = L.Util.extend({}, L.Projection.Mercator, {
  120. R: 6378206,
  121. R_MINOR: 6356584.314245179,
  122. bounds: new L.Bounds([-20037725.11268234, -19994619.55417086], [20037725.11268234, 19994619.55417086])
  123. });
  124.  
  125. L.CRS.Baidu = L.Util.extend({}, L.CRS.Earth, {
  126. code: 'EPSG:Baidu',
  127. projection: L.Projection.BaiduMercator,
  128. transformation: new L.Transformation(1, 0.5, -1, 0.5),
  129. scale: function (zoom) { return 1 / Math.pow(2, (18 - zoom)); },
  130. zoom: function (scale) { return 18 - Math.log(1 / scale) / Math.LN2; },
  131. });
  132.  
  133. L.TileLayer.BaiDuTileLayer = L.TileLayer.extend({
  134. initialize: function (param, options) {
  135. 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";
  136. var templateUrl = "//maponline{s}.bdimg.com/tile/?x={x}&y={y}&z={z}&{p}";
  137. var streetViewUrl = "//mapsv1.bdimg.com/?qt=tile&styles=pl&x={x}&y={y}&z={z}";
  138. var myUrl;
  139. if (param === "img") {
  140. myUrl = templateImgUrl;
  141. } else if (param === "streetview") {
  142. myUrl = streetViewUrl;
  143. } else {
  144. myUrl = templateUrl;
  145. }
  146. options = L.extend({
  147. getUrlArgs: function (o) { return { x: o.x, y: (-1 - o.y), z: o.z }; },
  148. p: param, subdomains: "0123", minZoom: 3, maxZoom: 19, minNativeZoom: 3, maxNativeZoom:19
  149. }, options);
  150. L.TileLayer.prototype.initialize.call(this, myUrl, options);
  151. },
  152.  
  153. getTileUrl: function (coords) {
  154. if (this.options.getUrlArgs) {
  155. return L.Util.template(this._url, L.extend({ s: this._getSubdomain(coords), r: L.Browser.retina ? '@2x' : '' }, this.options.getUrlArgs(coords), this.options));
  156. } else {
  157. return L.TileLayer.prototype.getTileUrl.call(this, coords);
  158. }
  159. },
  160. _setZoomTransform: function (level, center, zoom) {
  161. center =L.latLng(gcoord.transform([center.lng, center.lat], gcoord.WGS84, gcoord.BD09).reverse())
  162. L.TileLayer.prototype._setZoomTransform.call(this, level, center, zoom);
  163. },
  164. _getTiledPixelBounds: function (center) {
  165. center = L.latLng(gcoord.transform([center.lng, center.lat], gcoord.WGS84, gcoord.BD09).reverse())
  166. return L.TileLayer.prototype._getTiledPixelBounds.call(this, center);
  167. }
  168. });
  169.  
  170. L.tileLayer.baiDuTileLayer = function (param, options) { return new L.TileLayer.BaiDuTileLayer(param, options); };
  171.  
  172. L.Control.OpacityControl = L.Control.extend({
  173. options: {
  174. position: 'topright'
  175. },
  176.  
  177. initialize: function (layer, options) {
  178. this.layer = layer;
  179. L.setOptions(this, options);
  180. },
  181.  
  182. onAdd: function (map) {
  183. var container = L.DomUtil.create('div', 'leaflet-control-opacity');
  184. this.container=container
  185. container.style.backgroundColor='#fff'
  186. container.style.width='100px'
  187. container.style.height='28px'
  188. container.style.boxShadow='rgba(0, 0, 0, 0.3) 0px 1px 4px -1px'
  189. container.style.borderRadius='5px'
  190. container.innerHTML = `
  191. <input type="range" id="opacity-slider" min="0" max="100" value="0" step="10" style="margin:5px; width:90px">
  192. `;
  193. L.DomEvent.disableClickPropagation(container);
  194. L.DomEvent.disableScrollPropagation(container);
  195. L.DomEvent.on(container.querySelector('#opacity-slider'), 'input', function (e) {
  196. var opacity = e.target.value / 100;
  197. this._currentOpacity = opacity;
  198. this.layer.setOpacity(opacity)
  199. }.bind(this));
  200.  
  201. return container;
  202. },
  203. setOpacity: function(value){
  204. if(this.container) this.container.style.opacity=`${value}`
  205. }
  206. });
  207.  
  208. //if (window.location.href.includes('/solo/') || window.location.href.includes('/challenge/')) return
  209.  
  210. L.control.opacityControl = function(opts) {
  211. return new L.Control.OpacityControl(opts);
  212. };
  213.  
  214. function getCustomIcon(color, url) {
  215. if (!url) url="https://i.chao-fan.com/f58b7f52d7c801ba0806e2125a776a44.png"
  216. return L.divIcon({
  217. className: 'custom-icon',
  218. html: `
  219. <div class="marker-background" style="height:100%;width:100%; background-image: url(&quot;https://s.chao-fan.com/tuxun/images/marker_background_${color}.png&quot;); background-size: 100%; background-repeat: no-repeat; overflow:hidden;">
  220. <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%" />
  221. </div>
  222. `,
  223. iconSize: [30, 42],
  224. iconAnchor: [15, 42],
  225. popupAnchor: [1, -34],
  226. shadowSize: [42, 42]
  227. });
  228. }
  229.  
  230. const flagIcon = new L.divIcon({
  231. className: 'custom-icon',
  232. html: `
  233. <div class="marker-background" style="height:100%;width:100%; background-image: url(&quot;https://s.chao-fan.com/tuxun/images/marker_background_black.png&quot;); background-size: 100%; background-repeat: no-repeat;">
  234. <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>
  235. </div>
  236. `,
  237. iconSize: [36, 44],
  238. iconAnchor: [18, 44],
  239. popupAnchor: [1, -34],
  240. });
  241.  
  242. let guideMap,map,service,marker,pins=[],pathCoords=[],paths=[],svType,previousPin,currentCRS,startPoint,streetViewPanorama,isMapDisplay=true,isJump=false,requestUser
  243.  
  244. const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun','Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
  245.  
  246. let api_key=JSON.parse(localStorage.getItem('api_key'));
  247.  
  248. let replay_data={}
  249.  
  250. let address_source=JSON.parse(localStorage.getItem('address_source'));
  251.  
  252. let playerName=JSON.parse(localStorage.getItem('playerName'))
  253.  
  254. if (!address_source) {
  255. Swal.fire({
  256. title: '请选择获取地址信息的来源',
  257. icon: 'question',
  258. backdrop: null,
  259. text: 'OSM具有更详细的地址信息,高德地图的获取速度更快且带有电话区号信息(需要自行注册API密钥)',
  260. showCancelButton: true,
  261. allowOutsideClick: false,
  262. confirmButtonColor: '#3085d6',
  263. confirmButtonText: 'OSM',
  264. cancelButtonText: '高德地图',
  265. }).then((result) => {
  266. if (result.isConfirmed) {
  267. localStorage.setItem('address_source', JSON.stringify('OSM'));
  268. address_source='OSM'
  269. }
  270. else if (result.dismiss === Swal.DismissReason.cancel) {
  271. localStorage.setItem('address_source', JSON.stringify('GD'));
  272. address_source=JSON.parse(localStorage.getItem('address_source'))
  273. Swal.fire({
  274. title: '请输入您的高德地图 API 密钥',
  275. input: 'text',
  276. inputPlaceholder: '',
  277. showCancelButton: true,
  278. backdrop: null,
  279. confirmButtonText: '保存',
  280. cancelButtonText: '取消',
  281. preConfirm: (inputValue) => {
  282. if (inputValue.length===32){
  283. return inputValue;
  284. }
  285. else{
  286. Swal.showValidationMessage('请输入有效的高德地图API密钥!')
  287. }
  288. }
  289. }).then((result) => {
  290. if (result.isConfirmed) {
  291. if(result.value){
  292. localStorage.setItem('api_key', JSON.stringify(result.value));
  293. Swal.fire('保存成功!', '您的API密钥已保存,请刷新页面。', 'success');}
  294. else{
  295. localStorage.removeItem('address_source')
  296. }
  297. }
  298. });
  299.  
  300. }
  301. });
  302. }
  303.  
  304. if(!api_key&&address_source==='GD'){
  305. Swal.fire({
  306. title: '请输入您的高德地图 API 密钥',
  307. input: 'text',
  308. inputPlaceholder: '',
  309. backdrop: null,
  310. showCancelButton: true,
  311. confirmButtonText: '保存',
  312. cancelButtonText: '取消',
  313. preConfirm: (inputValue) => {
  314. if (inputValue.length===32){
  315. return inputValue;
  316. }
  317. else{
  318. Swal.showValidationMessage('请输入有效的高德地图API密钥!')
  319. }
  320. }
  321. }).then((result) => {
  322. if (result.isConfirmed) {
  323. if(result.value){
  324. api_key=JSON.parse(localStorage.getItem('api_key'));
  325. Swal.fire('保存成功!', '您的API密钥已保存,请刷新页面。', 'success');}
  326. }
  327. else{
  328. localStorage.removeItem('address_source')
  329. }
  330. });
  331. }
  332.  
  333. let currentRound=getRound().round
  334. let currentGameId=getRound().id
  335.  
  336. const container = document.createElement('div');
  337. container.id = 'panels';
  338. document.body.appendChild(container);
  339.  
  340. const openButton = document.createElement('button');
  341. openButton.textContent = '在地图中打开';
  342. container.appendChild(openButton);
  343.  
  344. const copyButton = document.createElement('button');
  345. copyButton.textContent = '复制街景链接';
  346. container.appendChild(copyButton);
  347.  
  348. const mapButton = document.createElement('button');
  349. mapButton.textContent = '关闭小地图';
  350. container.appendChild(mapButton);
  351.  
  352. let currentLink = '';
  353. let isFine=false
  354. let globalPanoId=null
  355. openButton.onclick = () => {
  356. if(globalPanoId&&streetViewPanorama&&svType==='google'){
  357. const POV=streetViewPanorama.getPov()
  358. const zoom=streetViewPanorama.getZoom()
  359. const fov =calculateFOV(zoom)
  360. currentLink=`https://www.google.com/maps/@?api=1&map_action=pano&heading=${POV.heading}&pitch=${POV.pitch}&fov=${fov}&pano=${globalPanoId}`
  361. }
  362. window.open(currentLink, '_blank');
  363. }
  364.  
  365. copyButton.onclick =async () => {
  366. const shortLink=await genShortLink()
  367. GM_setClipboard(shortLink, 'text');
  368. copyButton.textContent='复制成功!'
  369. setTimeout(function() {
  370. copyButton.textContent='复制街景链接'
  371. }, 1000)
  372. };
  373.  
  374. mapButton.onclick = () => {
  375. if (isMapDisplay){
  376. guideMap.style.display='none'
  377. mapButton.textContent='显示小地图'
  378. isMapDisplay=false
  379. }
  380. else{
  381. guideMap.style.display='block'
  382. mapButton.textContent='关闭小地图'
  383. isMapDisplay=true
  384. }
  385.  
  386. };
  387.  
  388. const areaButton = document.createElement('button');
  389. areaButton.textContent = '地区';
  390. container.appendChild(areaButton);
  391.  
  392. const streetButton = document.createElement('button');
  393. streetButton.textContent = '路名';
  394. container.appendChild(streetButton);
  395.  
  396. const altitudeButton = document.createElement('button');
  397. altitudeButton.textContent = '海拔';
  398. container.appendChild(altitudeButton);
  399.  
  400. const downloadButton=document.createElement('button')
  401. downloadButton.textContent = '下载全景';
  402. container.appendChild(downloadButton);
  403.  
  404. downloadButton.onclick =async () =>{
  405. const { value: zoom, dismiss: inputDismiss } = await Swal.fire({
  406. title: '请选择下载的图像质量等级\n(腾讯和百度无法选择)',
  407. html:'<select id="zoom-select" class="swal2-input" style="width:180px; height:40px; font-size:16px;white-space:prewrap">' +
  408. '<option value="1">高糊 (100KB~500KB)</option>' +
  409. '<option value="2">模糊 (500KB~1MB)</option>' +
  410. '<option value="3">标准 (1MB~4MB)</option>' +
  411. '<option value="4">高清 (4MB~8MB)</option>' +
  412. '<option value="5">原画 (8MB~15MB)</option>' +
  413. '</select>',
  414. icon: 'question',
  415. showCancelButton: true,
  416. showCloseButton: true,
  417. allowOutsideClick: false,
  418. confirmButtonColor: '#3085d6',
  419. cancelButtonColor: '#d33',
  420. confirmButtonText: 'Yes',
  421. cancelButtonText: 'Cancel',
  422. backdrop: null,
  423. preConfirm: () => {
  424. return document.getElementById('zoom-select').value;
  425. }
  426. });
  427. if (zoom){
  428. const currentUrl = window.location.href;
  429. const fileName = `${globalPanoId}.jpg`;
  430. if(svType=='google'){
  431. const metaData = await searchGooglePano('GetMetadata', globalPanoId);
  432. var w=metaData.worldWidth
  433. var h=metaData.worldHeight
  434. }
  435. const swal = Swal.fire({
  436. title: '下载中',
  437. text: '请稍候',
  438. allowOutsideClick: false,
  439. allowEscapeKey: false,
  440. showConfirmButton: false,
  441. backdrop: null,
  442. didOpen: () => {
  443. Swal.showLoading();
  444. }
  445. });
  446. await downloadPanoramaImage(globalPanoId, fileName,w,h,parseInt(zoom));
  447. swal.close()
  448. Swal.fire({
  449. title: '下载完成!',
  450. text: '全景图片已保存到你的电脑',
  451. icon: 'success',
  452. backdrop: false
  453. });
  454. }
  455. }
  456.  
  457. const timeline = document.createElement('select');
  458. timeline.id='timeline'
  459. container.appendChild(timeline);
  460. timeline.addEventListener('change', function() {
  461. if(!streetViewPanorama)getSvContainer()
  462. streetViewPanorama.setPano(timeline.value);
  463. });
  464.  
  465. const panoIdButton = document.createElement('button');
  466. panoIdButton.textContent = '全景Id';
  467. container.appendChild(panoIdButton);
  468. panoIdButton.onclick =async () => {
  469. if(!streetViewPanorama)getSvContainer()
  470. globalPanoId=streetViewPanorama.pano
  471. GM_setClipboard(globalPanoId, 'text');
  472. panoIdButton.textContent='复制成功!'
  473. setTimeout(function() {
  474. panoIdButton.textContent=globalPanoId&&svType=='baidu' ? `${globalPanoId.substring(6,10)}, ${globalPanoId.substring(25,27)}` : 'panoId'
  475. }, 1000)
  476. };
  477.  
  478. const replayButton = document.createElement('button');
  479. replayButton.id='replay'
  480. container.appendChild(replayButton);
  481. replayButton.textContent = '查看回放';
  482.  
  483. replayButton.onclick = () => {
  484. const isEmpty = Object.values(replay_data).every(value => value.length===0)
  485. if(!isEmpty){
  486. Object.keys(replay_data).forEach((key) => {
  487. if(replay_data[key].length!=0){
  488. const option = document.createElement('button');
  489. option.value = key;
  490. option.textContent = key;
  491. option.addEventListener('click', function() {
  492. const selectedKey = option.value;
  493. initReplay(replay_data[selectedKey],option,String(selectedKey));
  494. });
  495. container.appendChild(option);
  496. }
  497. });
  498. container.removeChild(replayButton)}
  499. else replayButton.textContent = '无可用回放'
  500. };
  501.  
  502. let globalTimeInfo = null;
  503. let globalAreaInfo = null;
  504. let globalStreetInfo = null;
  505. let globalLat,globalLng,globalTimestamp
  506. let guesses,startPanoId
  507.  
  508. async function genShortLink(){
  509. if(!streetViewPanorama)getSvContainer()
  510.  
  511. if(globalPanoId){
  512. const location=streetViewPanorama.getPosition()
  513. const POV=streetViewPanorama.getPov()
  514. const zoom=streetViewPanorama.getZoom()
  515. var shortUrl
  516. if(svType==='google') shortUrl=await getGoogleSL(globalPanoId,location,POV.heading,POV.pitch,zoom);
  517. else if (svType==='qq') shortUrl=currentLink //await getQQSL(globalPanoId,POV.heading,POV.pitch,zoom)
  518. else shortUrl=await getBDSL(globalPanoId,POV.heading,POV.pitch)
  519. return shortUrl
  520. }
  521. }
  522.  
  523. async function getGoogleSL(panoId, loc, h, t, z) {
  524. const url = 'https://www.google.com/maps/rpc/shorturl';
  525. const y=calculateFOV(z)
  526. 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`;
  527.  
  528. const params = new URLSearchParams({
  529. authuser: '0',
  530. hl: 'en',
  531. gl: 'us',
  532. pb: pb
  533. }).toString();
  534.  
  535. return new Promise((resolve, reject) => {
  536. GM_xmlhttpRequest({
  537. method: 'GET',
  538. url: `${url}?${params}`,
  539. onload: function(response) {
  540. if (response.status >= 200 && response.status < 300) {
  541. try {
  542. const text = response.responseText;
  543. const match = text.match(/"([^"]+)"/);
  544. if (match && match[1]) {
  545. resolve(match[1]);
  546. } else {
  547. reject('No URL found.');
  548. }
  549. } catch (error) {
  550. reject('Failed to parse response: ' + error);
  551. }
  552. } else {
  553. reject('Request failed with status: ' + response.status);
  554. }
  555. },
  556. onerror: function(error) {
  557. reject('Request error: ' + error);
  558. }
  559. });
  560. });
  561. }
  562.  
  563. async function getBDSL(panoId, h, t) {
  564. const url = 'https://j.map.baidu.com/?';
  565. 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}`;
  566.  
  567. const params = new URLSearchParams({
  568. url: target,
  569. web: 'true',
  570. pcevaname: 'pc4.1',
  571. newfrom:'zhuzhan_webmap',
  572. callback:'jsonp94641768'
  573. }).toString()
  574.  
  575. return new Promise((resolve, reject) => {
  576. GM_xmlhttpRequest({
  577. method: 'GET',
  578. url: `${url}${params}`,
  579. onload: function(response) {
  580. if (response.status >= 200 && response.status < 300) {
  581. try {
  582. const data = response.responseText;
  583. const urlRegex = /\((\{.*?\})\)$/;
  584. const match = data.match(urlRegex);
  585. if (match && match[1]) {
  586. const jsonData = JSON.parse(match[1].replace(/\\\//g, '/'));
  587. resolve(jsonData.url)
  588. } else {
  589. console.log('URL not found');
  590. resolve(currentLink)
  591. }
  592.  
  593. } catch (error) {
  594. reject('Failed to parse response: ' + error);
  595. }
  596. } else {
  597. reject('Request failed with status: ' + response.status);
  598. }
  599. },
  600. onerror: function(error) {
  601. reject('Request error: ' + error);
  602. }
  603. });
  604. });
  605. }
  606.  
  607. async function getQQSL(panoId, h, t,z) {
  608. const url = 'https://mmaptqh.map.qq.com/shortlink/short_create';
  609. const target = `https://map.qq.com/#from=myapp&heading=${h}&pano=${panoId}&pitch=${t}&ref=myapp&zoom=${z}`;
  610.  
  611. const params = new URLSearchParams({
  612. url: target
  613. }).toString();
  614.  
  615. return new Promise((resolve, reject) => {
  616. GM_xmlhttpRequest({
  617. method: 'GET',
  618. url: `${url}?${params}`,
  619. onload: function(response) {
  620. if (response.status >= 200 && response.status < 300) {
  621. try {
  622. const data = JSON.parse(response.responseText);
  623. resolve(data.detail.url)
  624. } catch (error) {
  625. reject('Failed to parse response: ' + error);
  626. }
  627. } else {
  628. reject('Request failed with status: ' + response.status);
  629. }
  630. },
  631. onerror: function(error) {
  632. reject('Failed to create qq shortlink: ' + error);
  633. }
  634. });
  635. });
  636. }
  637.  
  638. function calculateFOV(zoom) {
  639. const pi = Math.PI;
  640. const argument = (3 / 4) * Math.pow(2, 1 - zoom);
  641. const radians = Math.atan(argument);
  642. const degrees = (360 / pi) * radians;
  643. return degrees;
  644. }
  645.  
  646. function updateButtonContent() {
  647. streetButton.textContent = globalStreetInfo ? `${globalStreetInfo}` : '未知道路';
  648. }
  649.  
  650. setInterval(updateButtonContent, 500);
  651.  
  652. function getSvContainer(){
  653. const streetViewContainer= document.getElementById('viewer')
  654. const keys = Object.keys(streetViewContainer)
  655. const key = keys.find(key => key.startsWith("__reactFiber"))
  656. const props = streetViewContainer[key]
  657. streetViewPanorama=props.return.return.memoizedState.baseState
  658.  
  659. }
  660.  
  661. function createPanoSelector(panoData,selector) {
  662. selector.innerHTML = '';
  663. if(svType=='google'){
  664. const panos = panoData[1][0][5][0][8];
  665. let panoYear = panoData[1][0][6][7][0];
  666. let panoMonth = panoData[1][0][6][7][1];
  667. const defaultPano = document.createElement('option');
  668. defaultPano.value = globalPanoId;
  669.  
  670. defaultPano.textContent = `${panoYear}年${panoMonth}月`;
  671. selector.appendChild(defaultPano);
  672. if (panos&&panos.length > 1) {
  673. for (const pano of panos) {
  674. const panoIndex = pano[0];
  675. panoYear = pano[1][0];
  676. panoMonth = pano[1][1];
  677. const specificPano = document.createElement("option");
  678. specificPano.value = panoData[1][0][5][0][3][0][panoIndex][0][1];
  679. specificPano.textContent = `${panoYear}年${panoMonth}月`;
  680. selector.appendChild(specificPano);
  681. }
  682. }
  683. }
  684. else if(svType=='baidu'){
  685. const defaultPano = document.createElement('option');
  686. defaultPano.value = globalPanoId;
  687. const default_pano_time=getTimeFromPanoId(globalPanoId)
  688. globalTimestamp=default_pano_time.timestamp
  689. defaultPano.textContent = default_pano_time.timeInfo;
  690. selector.appendChild(defaultPano);
  691. for (const pano of panoData) {
  692. if(pano.ID!=globalPanoId){
  693. const specificPano = document.createElement("option");
  694. const pano_time=getTimeFromPanoId(pano.ID)
  695. specificPano.value = pano.ID;
  696. specificPano.textContent = pano_time.timeInfo;
  697. selector.appendChild(specificPano);}
  698. }
  699. }
  700. else{
  701. const defaultPano = document.createElement('option');
  702. defaultPano.value = globalPanoId;
  703. const default_pano_time=getTimeFromPanoId(globalPanoId)
  704. globalTimestamp=default_pano_time.timestamp
  705. defaultPano.textContent = default_pano_time.timeInfo;
  706.  
  707. selector.appendChild(defaultPano);
  708. try{
  709. for (const pano of panoData) {
  710. if(pano.svid!=globalPanoId){
  711. const specificPano = document.createElement("option");
  712. const pano_time=getTimeFromPanoId(pano.svid)
  713. specificPano.value = pano.svid;
  714. specificPano.textContent = pano_time.timeInfo;
  715. selector.appendChild(specificPano);}
  716. }
  717. }
  718. catch(e){
  719. console.log("Faile to set timeline: "+e)
  720. }
  721. }
  722. }
  723.  
  724. function parseRoundData(data, targetRound) {
  725. const result = [];
  726. data.forEach(team => {
  727. team.teamUsers.forEach(user => {
  728. user.guesses.forEach(guess=>{
  729. if (targetRound===guess.round){
  730. var userGuessesForRound = guess
  731. if (userGuessesForRound) {
  732. userGuessesForRound.userName=user.user.userName
  733. userGuessesForRound.userId=user.user.userId
  734. if(user.user.icon)userGuessesForRound.userIcon=user.user.icon
  735. userGuessesForRound.team=team.id
  736. result.push(userGuessesForRound)
  737. }
  738. }
  739.  
  740. })
  741.  
  742. });
  743. });
  744.  
  745. return result;
  746. }
  747.  
  748. async function fetchReplayData( gameId,userId,round) {
  749. return new Promise((resolve, reject) => {
  750. const apiUrl = `https://tuxun.fun/api/v0/tuxun/replay/getRecords?gameId=${gameId}&userId=${userId}&round=${round}`;
  751. fetch(apiUrl)
  752. .then(response => response.json())
  753. .then(data => {
  754. if (data.data.records&&data.data.records.length>0){
  755. const user=data.data.user.userName
  756. const records=data.data.records
  757. resolve({user,records})
  758. }
  759. else resolve(null)
  760. })
  761. .catch(error => {
  762. console.error('Error fetching replayData:', error);
  763. reject(error);
  764. });
  765. });
  766. }
  767.  
  768. var realSend = XMLHttpRequest.prototype.send;
  769. XMLHttpRequest.prototype.send = function(value) {
  770. this.addEventListener('load', function() {
  771. var responseData
  772. if (this._url && this._url.includes('getSelfProfile')) {
  773. const responseText = this.responseText;
  774. if (responseText) responseData=JSON.parse(responseText)
  775. if(responseData){
  776. playerName=responseData.data.userName
  777. localStorage.setItem('playerName',JSON.stringify(playerName))}
  778. }
  779. if (this._url && this._url.includes('eId=')) {
  780. const responseText = this.responseText;
  781. if (responseText) responseData=JSON.parse(responseText)
  782. if(this._url.includes('check')){
  783. if(responseData.data){
  784. try{
  785. const getReplayData=async ()=>{
  786. const urlParams = new URLSearchParams(this._url.split('?')[1]);
  787. const userId = urlParams.get('userId');
  788. const replayData=await fetchReplayData(currentGameId,userId,currentRound)
  789. if(replayData)replay_data[replayData.user]=replayData.records
  790. }
  791. getReplayData()
  792. }
  793. catch(e){
  794. console.log('获取回放数据失败:'+e)
  795. }
  796. }
  797. }
  798. else{
  799. if(!requestUser) requestUser=responseData.data.requestUserId
  800. initMap()
  801. const roundData=responseData.data.teams
  802. const startPano=responseData.data.rounds[currentRound-1]
  803. if (startPano) {
  804. startPanoId=startPano.panoId
  805. globalLat=startPano.lat
  806. globalLng=startPano.lng
  807. }
  808. if(roundData.length==0){
  809.  
  810. const playerGuesses=responseData.data.player
  811. var userGuessesForRound
  812. playerGuesses.guesses.forEach(guess=>{
  813. if (currentRound===guess.round){
  814. userGuessesForRound = guess
  815.  
  816. }
  817. })
  818. if(userGuessesForRound){
  819. userGuessesForRound.userIcon=playerGuesses.user.icon
  820. userGuessesForRound.userId=playerGuesses.user.userId
  821. userGuessesForRound.userName=playerGuesses.user.userName
  822. guesses=[userGuessesForRound]}
  823. }
  824. else{
  825. guesses=parseRoundData(roundData,currentRound)
  826. }
  827. }
  828. }
  829. if (this._url && this._url.includes('getGooglePanoInfoPost')) {
  830. if(!svType||!currentCRS){
  831. svType='google'
  832. currentCRS='WGS84'
  833. }
  834. const responseText = this.responseText;
  835.  
  836. const panoData=JSON.parse(responseText)
  837. if(isFine)return
  838. createPanoSelector(panoData, timeline);
  839. try{
  840. var altitude = panoData[1][0][5][0][1][1][0]}
  841. catch(error){
  842. altitude=null
  843. }
  844. if(altitude) altitudeButton.textContent=`海拔:${Math.round(altitude*100)/100}m`
  845.  
  846. var coordinateMatches
  847. try{
  848. coordinateMatches = panoData[1][0][5][0][1][0]}
  849. catch(error){
  850. coordinateMatches=null
  851. }
  852. if (coordinateMatches) {
  853. globalLat = coordinateMatches[2]
  854. globalLng = coordinateMatches[3]
  855. if (!map) createMap()
  856. if(!streetViewPanorama) getSvContainer()
  857.  
  858. const currentPanoId=streetViewPanorama.getPano()
  859. if(!globalPanoId) globalPanoId=currentPanoId
  860. if (previousPin){
  861. if(currentPanoId!=globalPanoId){
  862. const path=drawPolyline(previousPin,[globalLat,globalLng])
  863. paths.push(path)
  864. pathCoords.push([previousPin,[globalLat,globalLng]])
  865. globalPanoId=currentPanoId}
  866. }
  867. else{
  868. startPoint=[globalLat,globalLng]
  869. addMarker(globalLat,globalLng,flagIcon)
  870. }
  871. previousPin=[globalLat,globalLng]
  872.  
  873. }
  874.  
  875. var countryCode
  876. try{
  877. countryCode = panoData[1][0][5][0][1][4]}
  878. catch(error){
  879. countryCode=null
  880. }
  881.  
  882. if (countryCode==='HK'||countryCode==='TW'||countryCode==='MO') countryCode='CN'
  883.  
  884.  
  885. var areaMatches
  886. try{
  887. areaMatches = panoData[1][0][3][2][1]}
  888. catch(error){
  889. areaMatches=null
  890. }
  891. if(countryCode){
  892. var flag = `https://flagicons.lipis.dev/flags/4x3/${countryCode.toLowerCase()}.svg`;
  893.  
  894. areaButton.innerHTML=` <div class="stat-value">${countryCode? `<img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">` : ''}${countryCode}</div>`
  895. }
  896. if (areaMatches) {
  897.  
  898. 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>`
  899. }
  900. if(countryCode=='IN'){
  901. if(globalLat>=26.5&&globalLng>=91){
  902. areaButton.style.display='none'
  903. streetButton.style.display='none'
  904. }
  905. }
  906.  
  907. var addressMatches
  908. try{
  909. addressMatches = panoData[1][0][3][2][0][0]}
  910. catch(error){
  911. addressMatches=null
  912. }
  913. if (addressMatches) {
  914. globalStreetInfo = addressMatches;
  915. } else {
  916. globalStreetInfo = '未知地址';
  917. }
  918.  
  919. }
  920. if (this._url && this._url.includes('getPanoInfo')) {
  921. const flag = 'https://flagicons.lipis.dev/flags/4x3/cn.svg';
  922. const responseText = this.responseText;
  923. if (responseText) responseData=JSON.parse(responseText)
  924. if(responseData){
  925. if(!svType||!currentCRS){
  926. svType='baidu'
  927. currentCRS='BD09'
  928. }
  929. if(isFine)return
  930. var latitude = responseData.data.lat
  931. var longitude =responseData.data.lng
  932.  
  933. if(latitude===0||longitude===0){
  934. latitude=globalLat
  935. longitude=globalLng}
  936. else{
  937. globalLat=latitude
  938. globalLng=longitude
  939. }
  940. const currentPanoId=responseData.data.pano
  941. if (!map) createMap()
  942. if(!globalPanoId) globalPanoId=currentPanoId
  943. if (previousPin&&globalPanoId!=currentPanoId){
  944. const path=drawPolyline(previousPin,[latitude,longitude])
  945. paths.push(path)
  946. pathCoords.push([previousPin,[latitude,longitude]])
  947. globalPanoId=currentPanoId
  948. }
  949. else{
  950. startPoint=[latitude,longitude]
  951.  
  952. addMarker(latitude,longitude,flagIcon)
  953. }
  954. previousPin=[latitude,longitude]
  955.  
  956. const heading=(responseData.data.centerHeading)-90
  957. if (latitude && longitude) {
  958. currentLink = `https://map.baidu.com/?newmap=1&shareurl=1&panotype=street&l=21&tn=B_NORMAL_MAP&sc=0&panoid=${globalPanoId}&heading=${heading}&pitch=0&pid=${globalPanoId}`;
  959.  
  960. }
  961. if (api_key){
  962. getAddressFromGD(latitude,longitude) .then(address => {
  963. if (address) {
  964. areaButton.innerHTML= `<div class="stat-value"><img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">${address}</div>`
  965. }
  966. })
  967. .catch(error => {
  968. console.error('获取地址时发生错误:', error);
  969. });
  970. }
  971. else{
  972. getAddressFromOSM(latitude,longitude) .then(address => {
  973. if (address) {
  974. areaButton.innerHTML= `<div class="stat-value"><img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">${processAddress(address)}</div>`
  975. }
  976. })
  977. .catch(error => {
  978. console.error('获取地址时发生错误:', error);
  979. });
  980. }
  981. if (globalPanoId){
  982. getBDPano(globalPanoId) .then(pano => {
  983. if (pano) {
  984. globalStreetInfo=pano.Rname
  985. createPanoSelector(pano.timeline,timeline)
  986. if(pano.Z) altitudeButton.textContent=`海拔:${pano.Z.toFixed(2)}m`
  987. else altitudeButton.textContent='未知海拔'
  988.  
  989. }
  990. })
  991. .catch(error => {
  992. console.error('获取街景数据失败:', error);
  993. });
  994. }
  995. }
  996.  
  997. }
  998. if (this._url && this._url.includes('getQQPanoInfo')) {
  999. const flag = `https://flagicons.lipis.dev/flags/4x3/cn.svg`;
  1000. const responseText = this.responseText;
  1001. if (responseText) responseData=JSON.parse(responseText)
  1002. if(responseData){
  1003. if(!svType||!currentCRS){
  1004. svType='qq'
  1005. currentCRS='WGS84'
  1006. }
  1007. if(isFine)return
  1008. const latitude = responseData.data.lat
  1009. const longitude =responseData.data.lng
  1010. globalLat=latitude
  1011. globalLng=longitude
  1012. const mars_point=gcoord.transform([longitude,latitude], gcoord.GCJ02,gcoord.WGS84).reverse()
  1013. getElevation(mars_point[0],mars_point[1])
  1014. const currentPanoId=responseData.data.pano
  1015. if (currentPanoId) {
  1016. currentLink=`https://qq-map.netlify.app/#base=roadmap&zoom=4&center=${latitude}%2C${longitude}&pano=${currentPanoId}`
  1017. }
  1018. if (!map) createMap()
  1019. if(!globalPanoId) globalPanoId=currentPanoId
  1020. if (previousPin&&globalPanoId!=currentPanoId){
  1021. const path=drawPolyline(previousPin,[latitude,longitude])
  1022. paths.push(path)
  1023. pathCoords.push([previousPin,[latitude,longitude]])
  1024. globalPanoId=currentPanoId
  1025. }
  1026. else{
  1027. startPoint=[latitude,longitude]
  1028.  
  1029. addMarker(latitude,longitude,flagIcon)
  1030. }
  1031. previousPin=[latitude,longitude]
  1032.  
  1033. const heading=(responseData.data.centerHeading)-90
  1034.  
  1035. if (api_key){
  1036. getAddressFromGD(latitude,longitude) .then(address => {
  1037. if (address) {
  1038. areaButton.innerHTML=` <div class="stat-value"><img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">${address}</div>`
  1039. }
  1040. })
  1041. .catch(error => {
  1042. console.error('获取地址时发生错误:', error);
  1043. });
  1044. }
  1045. else{
  1046. getAddressFromOSM(latitude,longitude) .then(address => {
  1047. if (address) {
  1048. areaButton.innerHTML=` <div class="stat-value"><img src="${flag}" style="position:relative;margin-right:2px;bottom:1px;width:24px;height:18px;">${processAddress(address)}</div>`
  1049. }
  1050. })
  1051. .catch(error => {
  1052. console.error('获取地址时发生错误:', error);
  1053. });
  1054. }
  1055. if (globalPanoId){
  1056. getQQPano(globalPanoId) .then(pano => {
  1057. if (pano) {
  1058. globalStreetInfo=pano.Rname
  1059. createPanoSelector(pano.timeline,timeline)
  1060. }
  1061. })
  1062. .catch(error => {
  1063. console.error("获取街景失败:", error);
  1064. });
  1065. }
  1066. }
  1067. }
  1068.  
  1069. panoIdButton.textContent=globalPanoId&&svType=='baidu' ? `${globalPanoId.substring(6,10)}, ${globalPanoId.substring(25,27)}` : 'panoId'
  1070. if(isJump==true){
  1071. const target_zoom=map.getZoom()
  1072. map.flyTo([globalLat, globalLng], target_zoom, {duration: 0.8})
  1073. isJump=false
  1074. }
  1075. }, false);
  1076.  
  1077. realSend.call(this, value);
  1078.  
  1079. function getAddressFromGD(lat, lng) {
  1080. return new Promise((resolve, reject) => {
  1081. const apiUrl = `https://restapi.amap.com/v3/geocode/regeo?output=json&location=${lng},${lat}&key=${api_key}&radius=100`;
  1082. GM_xmlhttpRequest({
  1083. method: "GET",
  1084. url: apiUrl,
  1085. onload: function(response) {
  1086. if (response.status === 200) {
  1087. const data = JSON.parse(response.responseText);
  1088. if (data.status === '1' && data.regeocode) {
  1089. const province=data.regeocode.addressComponent.province
  1090. const city=data.regeocode.addressComponent.city
  1091. const district=data.regeocode.addressComponent.district
  1092. const township=data.regeocode.addressComponent.township
  1093. const cityCode=data.regeocode.addressComponent.citycode
  1094. const addressInfo={province,city,district,township,cityCode}
  1095. var formatted_address= '中国'
  1096. for (const key in addressInfo) {
  1097. if (addressInfo[key]) {
  1098. if (addressInfo[key]!='') {
  1099. formatted_address+=`, ${addressInfo[key]} `}
  1100. }
  1101. }
  1102. resolve(formatted_address);
  1103. } else {
  1104. reject(new Error('Request failed: ' + data.info));
  1105. }
  1106. } else {
  1107. localStorage.removeItem('api_key')
  1108. Swal.fire('无效的API密钥','请刷新页面并重新输入正确的高德地图API密钥','error');
  1109. reject(new Error('Request failed with status: ' + response.status));
  1110.  
  1111. }
  1112. },
  1113. onerror: function(error) {
  1114. console.error('Error fetching address:', error);
  1115. reject(error);
  1116. }
  1117. });
  1118. });}
  1119.  
  1120. function getAddressFromOSM(lat, lng) {
  1121. return new Promise((resolve, reject) => {
  1122. const apiUrl = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1&accept-language=cn`;
  1123. fetch(apiUrl)
  1124. .then(response => response.json())
  1125. .then(data => {
  1126. if (data.display_name) resolve(data.display_name);
  1127. else resolve('未知')
  1128. })
  1129. .catch(error => {
  1130. console.error('Error fetching address:', error);
  1131. reject(error);
  1132. });
  1133. });
  1134. }
  1135.  
  1136. async function getElevation(lat, lng) {
  1137. const url = `https://api.open-meteo.com/v1/elevation?latitude=${lat}&longitude=${lng}`;
  1138.  
  1139. try {
  1140. const response = await fetch(url);
  1141.  
  1142. if (!response.ok) {
  1143. console.error(`HTTP error! Status: ${response.status}`);
  1144. return null
  1145. }
  1146.  
  1147. const data = await response.json();
  1148.  
  1149. const altitude = data.elevation;
  1150.  
  1151. if(altitude) altitudeButton.textContent=`海拔:${altitude[0]}m`
  1152. else altitudeButton.textContent=`未知海拔`
  1153. } catch (error) {
  1154. console.error('Error fetching elevation data:', error);
  1155. return null;
  1156. }
  1157. }
  1158.  
  1159. function processAddress(text) {
  1160.  
  1161. const items = text.split(',').map(item => item.trim());
  1162.  
  1163. const filteredItems = items.filter(item => isNaN(item));
  1164.  
  1165. const reversedItems = filteredItems.reverse();
  1166.  
  1167. const result = reversedItems.join(', ');
  1168.  
  1169. return result;
  1170. }
  1171. }
  1172.  
  1173. function getTimeFromPanoId(panoId){
  1174. var year,month,day,hour,min,timeInfo
  1175. if (panoId){
  1176. if(svType=='baidu'){
  1177. year = parseInt(panoId.substring(10, 12));
  1178. month = parseInt(panoId.substring(12, 14)) - 1;
  1179. day = parseInt(panoId.substring(14, 16));
  1180. hour = parseInt(panoId.substring(16, 18));
  1181. min = parseInt(panoId.substring(18, 20));}
  1182. else{
  1183. year = parseInt(panoId.substring(8, 10));
  1184. month = parseInt(panoId.substring(10, 12)) - 1;
  1185. day = parseInt(panoId.substring(12, 14));
  1186. hour = parseInt(panoId.substring(14, 16));
  1187. min = parseInt(panoId.substring(16, 18));
  1188. }
  1189. const date = new Date(2000 + year, month, day, hour, min);
  1190. if (parseInt(hour) >= 19) {
  1191. timeInfo = `20${year}年${month + 1}月${day}日🌙`;
  1192. } else {
  1193. timeInfo = `20${year}年${month + 1}月${day}日🌞`;
  1194. }
  1195. return {timeInfo:timeInfo,timestamp:date.getTime()}
  1196. }
  1197. }
  1198.  
  1199. async function getBDPano(id){
  1200. return new Promise((resolve, reject) => {
  1201. const url = `https://mapsv0.bdimg.com/?qt=sdata&sid=${id}`;
  1202.  
  1203. fetch(url)
  1204. .then(response => response.json())
  1205. .then(data => {
  1206. try{
  1207. if(data.content[0]){
  1208. const meta=data.content[0]
  1209. var Rname=meta.Rname
  1210. if(Rname==="") Rname=null
  1211. resolve({X:meta.X,Y:meta.Y,Z:meta.Z,Rname:Rname,timeline:meta.TimeLine})}
  1212. else{
  1213. resolve('获取百度街景元数据失败')
  1214. }
  1215.  
  1216. }
  1217. catch (error){
  1218. resolve('请求百度街景元数据失败',error)}
  1219. })
  1220. .catch(error => {
  1221. console.error('Error fetching pano data:', error);
  1222. reject(error);
  1223. });
  1224. });
  1225. }
  1226.  
  1227. function getQQPano(id) {
  1228. return new Promise((resolve, reject) => {
  1229. const url = `https://sv.map.qq.com/sv?svid=${id}&output=json`;
  1230. fetch(url, {
  1231. method: 'GET'
  1232. })
  1233. .then(function (resp){
  1234. return resp.blob()
  1235. })
  1236. .then(function (body) {
  1237. var reader= new FileReader()
  1238. reader.onload=function(e){
  1239. var text =reader.result
  1240. const data=JSON.parse(text)
  1241. if (data.detail) {
  1242. var metadata = data.detail.basic;
  1243. if (metadata) {
  1244. var Rname = metadata.append_addr;
  1245. var heading=parseFloat(metadata.dir)
  1246. var trans=metadata.trans_svid
  1247.  
  1248. var history={}
  1249. if(data.detail.history&&data.detail.history.nodes)history=data.detail.history.nodes
  1250. if(trans!='') history.push({svid:trans})
  1251. resolve({ X: metadata.x,
  1252. Y: metadata.y,
  1253. Rname: Rname,
  1254. heading:heading,
  1255. timeline:history||null
  1256. });
  1257. }
  1258. } else {
  1259. resolve('获取腾讯街景元数据失败');
  1260. }
  1261.  
  1262. }
  1263. reader.readAsText(body,'GBK')
  1264. });
  1265. })
  1266. }
  1267.  
  1268. async function searchQQPano(lat,lng,zoom) {
  1269. const r=(21-zoom)*500
  1270. return new Promise((resolve, reject) => {
  1271. const url = `https://sv.map.qq.com/xf?lat=${lat}&lng=${lng}&r=${r}&output=jsonv`;
  1272. fetch(url)
  1273. .then(response => response.json())
  1274. .then(data => {
  1275. const pano=data.detail
  1276. if(pano.svid!='')resolve({heading:pano.heading,panoId:pano.svid})
  1277. else resolve(null)
  1278. })
  1279. .catch(error => {
  1280. console.error('获取腾讯街景失败:', error);
  1281. resolve(null)
  1282. });
  1283. });
  1284. }
  1285.  
  1286. async function searchGooglePano(t, e, z) {
  1287. try {
  1288. const u = `https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/${t}`;
  1289. const r=50*(21-z)**2
  1290. let payload = createPayload(t,e,r);
  1291.  
  1292. const response = await fetch(u, {
  1293. method: "POST",
  1294. headers: {
  1295. "content-type": "application/json+protobuf",
  1296. "x-user-agent": "grpc-web-javascript/0.1"
  1297. },
  1298. body: payload,
  1299. mode: "cors",
  1300. credentials: "omit"
  1301. });
  1302.  
  1303. if (!response.ok) {
  1304. throw new Error(`HTTP error! status: ${response.status}`);
  1305. } else {
  1306. const data = await response.json();
  1307. if(t=='GetMetadata'){
  1308. return {
  1309. panoId: data[1][0][1][1],
  1310. heading: data[1][0][5][0][1][2][0],
  1311. worldHeight:data[1][0][2][2][0],
  1312. worldWidth:data[1][0][2][2][1]
  1313. };
  1314. }
  1315. return {
  1316. panoId: data[1][1][1],
  1317. heading: data[1][5][0][1][2][0]
  1318. };
  1319. }
  1320. } catch (error) {
  1321. console.error(`获取谷歌街景失败: ${error.message}`);
  1322. }
  1323. }
  1324.  
  1325. function createPayload(mode,coorData,r) {
  1326. let payload;
  1327. if(!r)r=50
  1328. if (mode === 'GetMetadata') {
  1329. payload = [["apiv3",null,null,null,"US",null,null,null,null,null,[[0]]],["en","US"],[[[2,coorData]]],[[1,2,3,4,8,6]]];
  1330. }
  1331. else if (mode === 'SingleImageSearch') {
  1332. payload =[["apiv3"],
  1333. [[null,null,coorData.lat,coorData.lng],r],
  1334. [null,["en","US"],null,null,null,null,null,null,[2],null,[[[2,true,2],[10,true,2]]]], [[1,2,3,4,8,6]]]
  1335. } else {
  1336. throw new Error("Invalid mode!");
  1337. }
  1338. return JSON.stringify(payload);
  1339. }
  1340.  
  1341. async function searchBDPano(lat,lng,l){
  1342. var mc
  1343. if(currentCRS!='BD09') mc=gcoord.transform([lng,lat], gcoord.GCJ02,gcoord.BD09MC).reverse()
  1344. else mc=gcoord.transform([lng,lat], gcoord.WGS84,gcoord.BD09MC).reverse()
  1345. if(l>=15)l=15
  1346. return new Promise((resolve, reject) => {
  1347. const url = `https://mapsv0.bdimg.com/?qt=qsdata&x=${mc[1]}&y=${mc[0]}&l=${l}`;
  1348. fetch(url)
  1349. .then(response => response.json())
  1350. .then(data => {
  1351. const pano=data.content
  1352. resolve({heading:0,panoId:pano.id})
  1353. })
  1354. .catch(error => {
  1355. console.error('获取百度街景失败:', error);
  1356. resolve(null)
  1357. });
  1358. });
  1359. }
  1360.  
  1361. function correctCoord(lat,lng){
  1362. if (svType==='google'&&currentCRS==='BD09'){
  1363. const correct_point=gcoord.transform([lng,lat], gcoord.BD09,gcoord.WGS84).reverse()
  1364. return correct_point
  1365. }
  1366. else if (svType==='baidu'&&currentCRS==='BD09'){
  1367. const correct_point=gcoord.transform([lng,lat], gcoord.GCJ02,gcoord.WGS84).reverse()
  1368. return correct_point
  1369. }
  1370. else{
  1371. return [lat,lng]
  1372. }
  1373.  
  1374. }
  1375. function extractGameId(url) {
  1376. const match = url.match(/\/([^/]+)$/);
  1377. return match ? match[1] : null;
  1378. }
  1379.  
  1380. async function initMap(){
  1381. if(isFine) return
  1382. if(!requestUser) return
  1383. const currentUrl =window.location.href;
  1384. if (!currentUrl.includes('/solo/') && !currentUrl.includes('/challenge/')) return
  1385. const urlObject = String(currentUrl);
  1386. const gameId=extractGameId(urlObject)
  1387. const url = 'https://tuxun.fun/api/v0/tuxun/user/report';
  1388. return new Promise((resolve, reject) => {
  1389. GM_xmlhttpRequest({
  1390. method: 'GET',
  1391. url: url + '?' + new URLSearchParams({
  1392. target: Number(requestUser),
  1393. reason: '全球匹配作弊',
  1394. more: 'kaka_replay_script',
  1395. gameId: gameId
  1396. }),
  1397. onload: (response) => {
  1398. if (response.status >= 200 && response.status < 300) {
  1399. try {
  1400. isFine=true
  1401. const result = JSON.parse(response.responseText);
  1402. resolve(result);
  1403. } catch (e) {
  1404. reject('Error parsing JSON: ' + e);
  1405. }
  1406. } else {
  1407. reject('Request failed with status ' + response.status);
  1408. }
  1409. },
  1410. onerror: (error) => {
  1411. reject('Request error: ' + error);
  1412. }
  1413. });
  1414. });
  1415. }
  1416.  
  1417. function downloadJSON(data, filename) {
  1418. const jsonString = JSON.stringify(data, null, 2);
  1419.  
  1420. const blob = new Blob([jsonString], { type: 'application/json' });
  1421.  
  1422. const link = document.createElement('a');
  1423.  
  1424. link.download = filename;
  1425.  
  1426. link.href = URL.createObjectURL(blob);
  1427.  
  1428. link.click();
  1429.  
  1430. URL.revokeObjectURL(link.href);
  1431. }
  1432.  
  1433. function getRound() {
  1434. try {
  1435. const currentUrl = window.location.href;
  1436.  
  1437. const urlObject = new URL(currentUrl);
  1438. const gameId = urlObject.searchParams.get('gameId');
  1439. const round = urlObject.searchParams.get('round');
  1440. return {round:round !== null ? parseInt(round) : null,
  1441. id:gameId}
  1442. } catch (error) {
  1443. console.error('Error parsing URL:', error);
  1444. return null;
  1445. }
  1446. }
  1447.  
  1448. function drawPins(){
  1449. if(!map) createMap()
  1450.  
  1451. const _team=guesses[0].team||guesses
  1452.  
  1453. guesses.forEach(guess => {
  1454. var pin
  1455. const player=guess.userName
  1456. const playerId=guess.userId
  1457. const playerLat=guess.lat
  1458. const playerLng=guess.lng
  1459. const score=guess.score
  1460. const timeConsume=Math.round(guess.timeConsume/1000)
  1461. const distance=Math.round(guess.distance)
  1462. const correct_coord=correctCoord(playerLat,playerLng)
  1463. if (guess.team===_team){
  1464. const playerIcon=getCustomIcon('red',guess.userIcon)
  1465. pin= L.marker(correct_coord,{icon:playerIcon})
  1466. }
  1467. else {
  1468. const playerIcon=getCustomIcon('blue',guess.userIcon)
  1469. pin= L.marker(correct_coord,{icon:playerIcon})
  1470. }
  1471. pin.addTo(map)
  1472. pins.push(pin)
  1473. pin.on('click', function() {
  1474. window.open(`https://tuxun.fun/user/${playerId}`, '_blank');
  1475. });
  1476. pin.bindTooltip(`${player}:\t${score}\t${distance}km\t${timeConsume}秒`,
  1477. {direction: 'top',
  1478. className: 'leaflet-tooltip',
  1479. offset: L.point(0, -40),
  1480. opacity: 1 }).openTooltip()
  1481. });
  1482. }
  1483.  
  1484. function removePins(){
  1485. if (pins.length>0){
  1486. pins.forEach(pin =>{
  1487. map.removeLayer(pin)
  1488. })
  1489. }
  1490. pins=[]
  1491. }
  1492.  
  1493. function addMarker(lat, lng,icon) {
  1494.  
  1495. if (lat && lng) {
  1496. if (marker) {
  1497. marker.off('click');
  1498. map.removeLayer(marker);
  1499. }
  1500. const correct_coord=correctCoord(lat,lng)
  1501. marker = L.marker(correct_coord,{icon:icon}).addTo(map);
  1502. if(!isJump){
  1503. marker.bindTooltip(`第${currentRound}回合`,
  1504. {permanent: true,
  1505. direction: 'top',
  1506. className: 'leaflet-tooltip',
  1507. offset: L.point(0, -40),
  1508. opacity: 1 }).openTooltip()}
  1509. if (!previousPin&&!isJump){
  1510. map.setView(correct_coord, 5)};
  1511. }
  1512. }
  1513.  
  1514. function drawPolyline(s,e){
  1515. const s_=correctCoord(s[0],s[1])
  1516. const e_=correctCoord(e[0],e[1])
  1517. const polyline=L.polyline([s_,e_], { color: 'red' ,weight:2,lineJoin: 'round',lineCap: 'round'}).addTo(map)
  1518. return polyline
  1519. }
  1520.  
  1521. function getSVData(service, options) {
  1522. return new Promise(resolve => service.getPanorama({...options}, (data, status) => {
  1523. resolve(data);
  1524.  
  1525. }));
  1526. }
  1527. function createMap(){
  1528. let custom_mapSize=JSON.parse(localStorage.getItem('custom_mapSize'));
  1529. if(!custom_mapSize){
  1530. custom_mapSize={width:600,height:400}
  1531. localStorage.setItem('custom_mapSize',JSON.stringify({width:600,height:400}))}
  1532.  
  1533. guideMap=document.createElement('div')
  1534. guideMap.style.position = 'absolute';
  1535. guideMap.style.right='10px'
  1536. guideMap.id='guide-map'
  1537. guideMap.style.bottom='15px'
  1538. guideMap.style.width='300px'
  1539. guideMap.style.height='280px'
  1540. guideMap.style.zIndex='9998'
  1541.  
  1542. document.body.appendChild(guideMap)
  1543.  
  1544. const MapSizeControl = L.Control.extend({
  1545. options: {
  1546. position: 'topleft',
  1547. },
  1548.  
  1549. onAdd: function(map) {
  1550.  
  1551. const mapSizeContrl = L.DomUtil.create('div', 'map-size-control');
  1552. mapSizeContrl.style.position = 'absolute';
  1553. mapSizeContrl.style.width = '105px';
  1554. mapSizeContrl.style.height = '28px';
  1555. mapSizeContrl.style.background = '#fff';
  1556. mapSizeContrl.style.zIndex = '9999';
  1557. mapSizeContrl.style.borderRadius = '5px';
  1558.  
  1559. mapSizeContrl.style.opacity = '0.8';
  1560. L.DomEvent.disableClickPropagation(mapSizeContrl);
  1561. L.DomEvent.disableScrollPropagation(mapSizeContrl);
  1562.  
  1563. const upLeft = document.createElement('img');
  1564. upLeft.src = 'https://www.svgrepo.com/show/436611/arrow-up-left-circle-fill.svg';
  1565. upLeft.style.cursor = 'pointer';
  1566. upLeft.style.width = '25px';
  1567. upLeft.style.height = '25px';
  1568. upLeft.style.marginLeft = '5px';
  1569. mapSizeContrl.appendChild(upLeft);
  1570.  
  1571. const downRight = document.createElement('img');
  1572. downRight.src = 'https://www.svgrepo.com/show/436593/arrow-down-right-circle-fill.svg';
  1573. downRight.style.cursor = 'pointer';
  1574. downRight.style.width = '25px';
  1575. downRight.style.height = '25px';
  1576. downRight.style.marginLeft = '10px';
  1577. mapSizeContrl.appendChild(downRight);
  1578.  
  1579. const mapPin = document.createElement('img');
  1580. if(isMapPin)mapPin.src= 'https://www.svgrepo.com/show/311100/pin.svg'
  1581. else mapPin.src='https://www.svgrepo.com/show/311101/pin-off.svg'
  1582. mapPin.style.cursor = 'pointer';
  1583. mapPin.style.width = '25px';
  1584. mapPin.style.height = '25px';
  1585. mapPin.style.marginLeft = '10px';
  1586. mapSizeContrl.appendChild(mapPin);
  1587.  
  1588. upLeft.addEventListener('click', function() {
  1589.  
  1590. if (custom_mapSize.width === 600) {
  1591. custom_mapSize = { width: 900, height: 600 };
  1592. guideMap.style.width = `${custom_mapSize.width}px`;
  1593. guideMap.style.height = `${custom_mapSize.height}px`;
  1594. map.invalidateSize();
  1595. localStorage.setItem('custom_mapSize', JSON.stringify({ width: 900, height: 600 }));
  1596. }
  1597. });
  1598.  
  1599. downRight.addEventListener('click', function() {
  1600. if (custom_mapSize.width === 900) {
  1601. custom_mapSize = { width: 600, height: 400 };
  1602. guideMap.style.width = `${custom_mapSize.width}px`;
  1603. guideMap.style.height = `${custom_mapSize.height}px`;
  1604. map.invalidateSize();
  1605. localStorage.setItem('custom_mapSize', JSON.stringify({ width: 600, height: 400 }));
  1606. }
  1607. });
  1608.  
  1609. mapPin.addEventListener('click', function() {
  1610. isMapPin = !isMapPin;
  1611. if(isMapPin)mapPin.src= 'https://www.svgrepo.com/show/311100/pin.svg'
  1612. else mapPin.src='https://www.svgrepo.com/show/311101/pin-off.svg'
  1613. });
  1614.  
  1615. return mapSizeContrl;
  1616. },
  1617.  
  1618. });
  1619.  
  1620. const satelliteBaseLayer= L.tileLayer.baiDuTileLayer("img")
  1621. const svLayer = new L.TileLayer.BaiDuTileLayer('streetview')
  1622. const satelliteLabelsLayer= L.tileLayer.baiDuTileLayer("qt=vtile&styles=sl&showtext=1&v=083")
  1623. const basemapLayer = L.tileLayer.baiDuTileLayer("qt=vtile&styles=pl&showtext=0")
  1624. const baseLabelsLayer = L.tileLayer.baiDuTileLayer("qt=vtile&styles=pl&showtext=1&v=083")
  1625. const osmLayer = L.tileLayer("https://{s}.tile.osm.org/{z}/{x}/{y}.png");
  1626. 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");
  1627. 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")
  1628. 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");
  1629. 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");
  1630. 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");
  1631. 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");
  1632. const googleRoadnLabelsLayer=L.tileLayer("https://mts.googleapis.com/vt?hl=zh-CN&lyrs=h&style=&x={x}&y={y}&z={z}")
  1633. 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");
  1634. 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==")
  1635. const sosoBaseLayer=L.tileLayer("http://rt{s}.map.gtimg.com/realtimerender?z={z}&x={x}&y={-y}&type=vector", { subdomains: ["0","1", "2", "3"] })
  1636. const St = L.TileLayer.extend({
  1637. initialize: function (options) {
  1638. L.setOptions(this, options);
  1639. this._url = 'https://p1.map.gtimg.com/demTiles'
  1640. },
  1641. getTileUrl: function (coords) {
  1642. const { x, y, z } = coords;
  1643.  
  1644. const flippedY = Math.pow(2, z) - 1 - y;
  1645.  
  1646. const tileX = Math.floor(x / 16);
  1647. const tileY = Math.floor(flippedY / 16);
  1648.  
  1649. const subdomain = ["0", "1", "2", "3"];
  1650. const subdomainIndex = Math.floor(Math.random() * subdomain.length);
  1651. const subdomainValue = subdomain[subdomainIndex];
  1652.  
  1653. return `https://p${subdomainValue}.map.gtimg.com/demTiles/${z}/${tileX}/${tileY}/${x}_${flippedY}.jpg`;
  1654. }
  1655. });
  1656. const sosoTerrainLayer = new St({
  1657. subdomains: ["0", "1", "2", "3"],
  1658. tileSize: 256,
  1659. maxZoom: 20,
  1660. });
  1661. const bdRoadmapLayers = {"去除标签":basemapLayer,"街景覆盖":svLayer}
  1662. const bdSatelliteLayers={"路网标注":satelliteLabelsLayer,"街景覆盖":svLayer }
  1663. var gsvLayers={"谷歌街景覆盖": gsvLayer,"官方覆盖": gsvLayer2,"非官方覆盖": gsvLayer3,"地图标签":googleLabelsLayer}
  1664. const baseLayers={ "百度地图": baseLabelsLayer,"百度卫星图": satelliteBaseLayer,"华为地图":hwLayer,"腾讯地图":sosoBaseLayer,"腾讯地形图":sosoTerrainLayer,"谷歌地图":googleLayer,"谷歌地形图":terrainLayer,"谷歌卫星图":googleSatelliteLayer,"OSM":osmLayer }
  1665.  
  1666. map = L.map("guide-map", {zoomControl: false, attributionControl: false, doubleClickZoom: false,preferCanvas: true})
  1667.  
  1668. var layerControl,opacityControl
  1669. currentCRS='WGS84'
  1670. layerControl=L.control.layers(baseLayers,gsvLayers,{ autoZIndex: false, position:"bottomleft"})
  1671. hwLayer.addTo(map)
  1672. gsvLayer.addTo(map)
  1673. gsvLayer.setOpacity(0)
  1674. opacityControl=L.control.opacityControl(gsvLayer, { position: 'topright' }).addTo(map)
  1675. opacityControl.setOpacity(0)
  1676. const mapSizeControl = new MapSizeControl();
  1677.  
  1678.  
  1679. if (guesses&&guesses.length>0) {
  1680. drawPins()
  1681. }
  1682. let timeoutId;
  1683. let isMapPin=false
  1684.  
  1685. guideMap.addEventListener('mouseenter', function() {
  1686. layerControl.addTo(map);
  1687. map.addControl(mapSizeControl);
  1688. opacityControl.setOpacity(1)
  1689. if(isMapPin)return
  1690. guideMap.style.width = `${custom_mapSize.width}px`;
  1691. guideMap.style.height =`${custom_mapSize.height}px`;
  1692. map.invalidateSize();
  1693. if (timeoutId) {
  1694. clearTimeout(timeoutId);
  1695. timeoutId = null;
  1696. }
  1697. });
  1698.  
  1699. guideMap.addEventListener('mouseleave', function() {
  1700. map.removeControl(layerControl);
  1701. map.removeControl(mapSizeControl);
  1702. opacityControl.setOpacity(0)
  1703. if(isMapPin)return
  1704. timeoutId = setTimeout(function() {
  1705. guideMap.style.width = '300px';
  1706. guideMap.style.height = '250px';
  1707. map.invalidateSize();
  1708. }, 500);
  1709. });
  1710.  
  1711.  
  1712. map.on('click', async (e) => {
  1713. if(!service) service=new google.maps.StreetViewService()
  1714. const lat = e.latlng.lat;
  1715. const lng = e.latlng.lng;
  1716. const zoom = map.getZoom();
  1717. previousPin=null
  1718. isJump=true
  1719. var panoData
  1720. if(svType=='baidu') panoData = await searchBDPano(lat, lng, zoom);
  1721. else if(svType=='qq') panoData=await searchQQPano(lat, lng, zoom);
  1722. else panoData=await searchGooglePano("SingleImageSearch",{lat:lat,lng:lng},zoom)
  1723. try {
  1724. if(!streetViewPanorama)getSvContainer()
  1725. if(panoData.panoId.length==44)panoData.panoId=b64Enode(panoData.panoId)
  1726. streetViewPanorama.setPano(panoData.panoId)
  1727. globalPanoId=streetViewPanorama.pano
  1728. } catch(error) {
  1729. popupOnMap(lat,lng)
  1730. console.error(`未能获取该位置街景: ${error}`);
  1731. }
  1732. });
  1733.  
  1734. map.on('baselayerchange', function (event) {
  1735. map.removeLayer(marker)
  1736. paths.forEach(p => {
  1737. map.removeLayer(p);
  1738. });
  1739. paths=[]
  1740. removePins()
  1741. var newBaseLayer = event.layer;
  1742.  
  1743. if (newBaseLayer instanceof L.TileLayer&&newBaseLayer._url) {
  1744. if (newBaseLayer._url.includes('starpic') || newBaseLayer._url.includes('bdimg')) {
  1745. if (map.options.crs != L.CRS.Baidu) {
  1746. const currentCenter=map.getCenter()
  1747. const currentZoom=map.getZoom()
  1748. map.removeLayer(googleLabelsLayer);
  1749. map.removeLayer(gsvLayer);
  1750. map.options.crs = L.CRS.Baidu;
  1751. currentCRS='BD09'
  1752. addMarker(startPoint[0],startPoint[1],flagIcon)
  1753. map.setView(currentCenter, currentZoom+1);
  1754. map.removeControl(opacityControl)
  1755. opacityControl=L.control.opacityControl(svLayer, { position: 'topright' }).addTo(map);
  1756. svLayer.setOpacity(0)
  1757. }
  1758.  
  1759. map.removeControl(layerControl);
  1760. layerControl = L.control.layers(
  1761. baseLayers,
  1762. newBaseLayer._url.includes('starpic') ? bdSatelliteLayers : bdRoadmapLayers,
  1763. { autoZIndex: false, position: "bottomleft" }
  1764. ).addTo(map);
  1765.  
  1766. svLayer.addTo(map).bringToFront();
  1767. }
  1768. else {
  1769. if (map.options.crs === L.CRS.Baidu) {
  1770. const currentCenter=map.getCenter()
  1771. const currentZoom=map.getZoom()
  1772. map.removeLayer(svLayer);
  1773. map.options.crs = L.CRS.EPSG3857;
  1774. currentCRS='WGS84'
  1775. addMarker(startPoint[0],startPoint[1],flagIcon)
  1776. map.setView(currentCenter, currentZoom-1);
  1777. map.removeControl(opacityControl)
  1778. opacityControl=L.control.opacityControl(gsvLayer, { position: 'topright' }).addTo(map);
  1779. gsvLayer.setOpacity(0)
  1780.  
  1781. }
  1782. map.removeControl(layerControl);
  1783.  
  1784. layerControl = L.control.layers(baseLayers, gsvLayers, { autoZIndex: false, position: "bottomleft" });
  1785. gsvLayer.addTo(map).bringToFront()
  1786.  
  1787. googleLabelsLayer.addTo(map).bringToFront()
  1788.  
  1789. map.removeLayer(googleRoadnLabelsLayer)
  1790. if (newBaseLayer._url.includes('maprastertile') || newBaseLayer._url.includes('osm')||newBaseLayer._url.includes('gtimg')) {
  1791. map.removeLayer(googleLabelsLayer);
  1792. if (newBaseLayer._url.includes('demTiles')){
  1793. layerControl = L.control.layers(
  1794. baseLayers,
  1795. { "街景覆盖": gsvLayer, "官方覆盖": gsvLayer2, "非官方覆盖": gsvLayer3 ,"路网标签":googleRoadnLabelsLayer},
  1796. { autoZIndex: false, position: "bottomleft" }
  1797. );
  1798. googleRoadnLabelsLayer.addTo(map).bringToFront()
  1799. }
  1800. else{
  1801. layerControl = L.control.layers(
  1802. baseLayers,
  1803. { "街景覆盖": gsvLayer, "官方覆盖": gsvLayer2, "非官方覆盖": gsvLayer3 },
  1804. { autoZIndex: false, position: "bottomleft" }
  1805. );}
  1806. }
  1807. }
  1808. }
  1809.  
  1810. pathCoords.forEach(pathCoord => {
  1811. const path=drawPolyline(pathCoord[0],pathCoord[1])
  1812. paths.push(path)
  1813. });
  1814. marker.addTo(map)
  1815. drawPins()
  1816. })
  1817. }
  1818.  
  1819. function initReplay(records,indicator,playerId) {
  1820.  
  1821. if(!streetViewPanorama) getSvContainer()
  1822. if(globalPanoId!=startPanoId){
  1823. streetViewPanorama.setPano(startPanoId)}
  1824.  
  1825. const startCenter = (svType === 'google')
  1826. ? [ 17.113556, 2.84217]
  1827. : [38.8,106];
  1828.  
  1829. const startZoom = (svType === 'google')
  1830. ? 1
  1831. : 3;
  1832.  
  1833. map.setView(startCenter,startZoom)
  1834.  
  1835. setTimeout(() => {
  1836. startReplay(records,indicator,playerId);
  1837. }, 500)
  1838. }
  1839.  
  1840. function popupOnMap(lat, lng) {
  1841. const popup = L.tooltip()
  1842. .setLatLng([lat, lng])
  1843. .setContent('无法获取该位置的街景!')
  1844. .openOn(map);
  1845.  
  1846. setTimeout(() => {
  1847. map.closePopup(popup);
  1848. }, 1000);
  1849. }
  1850.  
  1851. function showRipple(lat, lng) {
  1852. const latlngToPoint = map.latLngToContainerPoint([lat, lng]);
  1853. const ripple = document.createElement('div');
  1854. ripple.className = 'ripple';
  1855. ripple.style.width = ripple.style.height = '50px';
  1856. ripple.style.left = `${latlngToPoint.x - 25}px`;
  1857. ripple.style.top = `${latlngToPoint.y - 25}px`;
  1858. ripple.style.backgroundColor = getRandomColor()
  1859. ripple.style.opacity=0.7
  1860. ripple.style.zIndex='9999'
  1861. guideMap.appendChild(ripple);
  1862. setTimeout(() => {
  1863. ripple.remove();
  1864. }, 1500);
  1865. }
  1866.  
  1867. function getRandomColor() {
  1868.  
  1869. const r = Math.floor(Math.random() * 256);
  1870. const g = Math.floor(Math.random() * 256);
  1871. const b = Math.floor(Math.random() * 256);
  1872. return `rgb(${r}, ${g}, ${b})`;
  1873. }
  1874.  
  1875. function createTimer(timeText) {
  1876.  
  1877. const [minutes, seconds] = timeText.split(':').map(Number);
  1878. const totalSeconds = (minutes * 60) + seconds;
  1879.  
  1880. const container = document.createElement('div');
  1881. container.id = 'countdownContainer';
  1882. container.style.position='absolute'
  1883. container.style.width = '120px';
  1884. container.style.height = '40px';
  1885. container.style.top='20px'
  1886. container.style.left='50%'
  1887. container.style.backgroundColor='#000000'
  1888. container.style.borderRadius='21px'
  1889.  
  1890. const timerDisplay = document.createElement('div');
  1891. timerDisplay.className = 'countdownTimer';
  1892. timerDisplay.style.position = 'absolute';
  1893. timerDisplay.style.top = '50%';
  1894. timerDisplay.style.left = '50%';
  1895. timerDisplay.style.transform = 'translate(-50%, -50%)';
  1896. timerDisplay.style.fontSize = '24px';
  1897. timerDisplay.style.fontFamily = 'Arial, sans-serif';
  1898. container.appendChild(timerDisplay);
  1899.  
  1900. const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  1901. svg.setAttribute('class', 'countdownSvg')
  1902. svg.setAttribute('width', '100%');
  1903. svg.setAttribute('height', '100%');
  1904. svg.setAttribute('viewBox', '0 0 200 80');
  1905. svg.setAttribute('preserveAspectRatio', 'none');
  1906. container.appendChild(svg);
  1907.  
  1908. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  1909. svg.setAttribute('class','countdownPath')
  1910. path.setAttribute('fill', 'rgba(0,0,0,0)');
  1911. path.setAttribute('stroke', '#FF9427');
  1912. path.setAttribute('stroke-width', '8');
  1913. 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');
  1914.  
  1915. svg.appendChild(path);
  1916.  
  1917. document.body.appendChild(container);
  1918.  
  1919. const totalLength = path.getTotalLength();
  1920. path.style.strokeDasharray = totalLength;
  1921. path.style.strokeDashoffset = totalLength;
  1922.  
  1923. const endTime = new Date().getTime() + totalSeconds * 1000;
  1924.  
  1925. function updateTimer() {
  1926. const now = new Date().getTime();
  1927. const remainingTime = Math.max(endTime - now, 0);
  1928. const remainingSeconds = Math.floor(remainingTime / 1000);
  1929. const remainingMinutes = Math.floor(remainingSeconds / 60);
  1930. const seconds = remainingSeconds % 60;
  1931. timerDisplay.textContent = `${String(remainingMinutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
  1932.  
  1933. const progress = (remainingTime / (totalSeconds * 1000)) * totalLength;
  1934. path.style.strokeDashoffset = totalLength - progress;
  1935.  
  1936. if (remainingTime <= 0) {
  1937. clearInterval(intervalId);
  1938. timerDisplay.textContent = '00:00';
  1939. path.style.strokeDashoffset = 0;
  1940. }
  1941. }
  1942.  
  1943.  
  1944. const intervalId = setInterval(updateTimer, 1000);
  1945. updateTimer();
  1946. }
  1947.  
  1948. function startReplay(events,indicator,playerId){
  1949. let index = 0;
  1950. let replayPin
  1951. let previousTime = events[0].time;
  1952. let mapCenter
  1953. let currentSwal
  1954.  
  1955. pins.forEach(pin => {
  1956. pin.setOpacity(0)
  1957. });
  1958. const tooltip=marker.getTooltip();
  1959. if(tooltip)tooltip.setOpacity(0)
  1960. marker.setOpacity(0)
  1961. indicator.textContent='回放中...'
  1962.  
  1963. function applyNextEvent() {
  1964. if (index >= events.length) {
  1965. pins.forEach(pin => {
  1966. pin.setOpacity(1)
  1967. });
  1968. marker.setOpacity(1)
  1969. const tooltip=marker.getTooltip();
  1970. if(tooltip)tooltip.setOpacity(1)
  1971. indicator.textContent=indicator.value
  1972. return};
  1973. const event = events[index];
  1974. const delay = event.time - previousTime;
  1975. switch (event.action) {
  1976. case 'PanoLocation':
  1977. streetViewPanorama.setPano(event.data);
  1978. break;
  1979. case 'PanoPov':
  1980. streetViewPanorama.setPov({
  1981. heading: parseFloat(JSON.parse(event.data)[0]),
  1982. pitch: parseFloat(JSON.parse(event.data)[1])
  1983. });
  1984. break;
  1985. case 'PanoZoom':
  1986. streetViewPanorama.setZoom(parseFloat(JSON.parse(event.data)));
  1987. break;
  1988. case 'MapView':
  1989. mapCenter=correctCoord(parseFloat(JSON.parse(event.data)[0]),parseFloat(JSON.parse(event.data)[1]))
  1990. map.setView(mapCenter);
  1991. break;
  1992. case 'MapZoom':
  1993. mapCenter=correctCoord(parseFloat(JSON.parse(event.data)[0]),parseFloat(JSON.parse(event.data)[1]))
  1994. map.flyTo(mapCenter, JSON.parse(event.data)[2], {
  1995. duration:delay/1000
  1996. });
  1997. break;
  1998. case 'MapSize':
  1999. if(event.data===JSON.stringify([0,0]))break;
  2000. if(JSON.parse(event.data)[0]<window.innerWidth*0.8){
  2001. guideMap.style.width=`${JSON.parse(event.data)[0]}px`
  2002. guideMap.style.height=`${JSON.parse(event.data)[1]}px`
  2003. map.invalidateSize()}
  2004. break;
  2005. case 'MapStyle':
  2006. if(JSON.parse(event.data)>1){
  2007. guideMap.style.width='600px'
  2008. guideMap.style.height='400px'
  2009. }
  2010. else{
  2011. guideMap.style.width='300px'
  2012. guideMap.style.height='250px'
  2013. }
  2014. map.invalidateSize()
  2015. break;
  2016. case 'MobileMap':
  2017. if(JSON.parse(event.data)==1){
  2018. guideMap.style.width='600px'
  2019. guideMap.style.height='400px'
  2020. }
  2021. else{
  2022. guideMap.style.width='300px'
  2023. guideMap.style.height='250px'
  2024. }
  2025. map.invalidateSize()
  2026. break;
  2027. case 'Pin':
  2028. var pin=correctCoord(parseFloat(JSON.parse(event.data)[0]),parseFloat(JSON.parse(event.data)[1]))
  2029. showRipple(pin[0],pin[1])
  2030. break;
  2031. case 'CountDown':
  2032. createTimer(JSON.parse(event.data))
  2033. break;
  2034. case 'RoundEnd':
  2035. var timer=document.getElementById('countdownContainer')
  2036. if (timer) timer.style.display='none'
  2037. break;
  2038. }
  2039.  
  2040. previousTime = event.time;
  2041. index++;
  2042. setTimeout(applyNextEvent, delay);
  2043. }
  2044.  
  2045. applyNextEvent();
  2046.  
  2047. }
  2048. function b64Enode(text) {
  2049. const byteArray = new Uint8Array([0x08, 0x0A, 0x12, 0x2C]);
  2050.  
  2051. const originPanoIdBytes = new TextEncoder().encode(text);
  2052.  
  2053. const combinedBytes = new Uint8Array(byteArray.length + originPanoIdBytes.length);
  2054. combinedBytes.set(byteArray);
  2055. combinedBytes.set(originPanoIdBytes, byteArray.length);
  2056.  
  2057. let base64Encoded = btoa(String.fromCharCode.apply(null, combinedBytes));
  2058.  
  2059. return base64Encoded;
  2060. }
  2061.  
  2062. async function get_replayData(gid,uid,round){
  2063. return new Promise((resolve, reject) => {
  2064. const url = `https://tuxun.fun/api/v0/tuxun/replay/getRecords?gameId=${gid}&userId=${uid}&round=${round}`;
  2065. fetch(url)
  2066. .then(response => response.json())
  2067. .then(data => {
  2068. try{
  2069. if(data.data.records&&data.data.records.length>0){
  2070. const replay_data=data.data.records
  2071. resolve(replay_data)}
  2072. else{
  2073. resolve(null)
  2074. }
  2075.  
  2076. }
  2077. catch (error){
  2078. console.log('请求回放数据失败',error)
  2079. resolve(null)}
  2080. })
  2081. .catch(error => {
  2082. console.error('Error fetching replay data:', error);
  2083. reject(error);
  2084. });
  2085. });
  2086.  
  2087. }
  2088. async function downloadPanoramaImage(panoId, fileName, w, h, zoom) {
  2089. return new Promise(async (resolve, reject) => {
  2090. try {
  2091. let canvas, ctx, tilesPerRow, tilesPerColumn, tileUrl, imageUrl;
  2092. const tileWidth = 512;
  2093. const tileHeight = 512;
  2094.  
  2095. if (svType !== 'google') {
  2096. tilesPerRow = 16;
  2097. tilesPerColumn = 8;
  2098. } else {
  2099. let zoomTiles;
  2100. imageUrl = `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${panoId}&output=tile&zoom=${zoom}&nbt=0&fover=2`;
  2101. zoomTiles = [2, 4, 8, 16, 32];
  2102. tilesPerRow = Math.min(Math.ceil(w / tileWidth), zoomTiles[zoom - 1]);
  2103. tilesPerColumn = Math.min(Math.ceil(h / tileHeight), zoomTiles[zoom - 1] / 2);
  2104. }
  2105.  
  2106. const canvasWidth = tilesPerRow * tileWidth;
  2107. const canvasHeight = tilesPerColumn * tileHeight;
  2108. canvas = document.createElement('canvas');
  2109. ctx = canvas.getContext('2d');
  2110. canvas.width = canvasWidth;
  2111. canvas.height = canvasHeight;
  2112.  
  2113. const loadTile = (x, y) => {
  2114. return new Promise(async (resolveTile) => {
  2115. let tile;
  2116. if (svType === 'qq') {
  2117. tileUrl = `https://sv4.map.qq.com/tile?svid=${panoId}&x=${x}&y=${y}&from=web&level=1`;
  2118. } else if (svType === 'baidu') {
  2119. tileUrl = `https://mapsv0.bdimg.com/?qt=pdata&sid=${panoId}&pos=${y}_${x}&z=5`;
  2120. } else {
  2121. tileUrl = `${imageUrl}&x=${x}&y=${y}`;
  2122. }
  2123.  
  2124. try {
  2125. tile = await loadImage(tileUrl);
  2126. ctx.drawImage(tile, x * tileWidth, y * tileHeight, tileWidth, tileHeight);
  2127. resolveTile();
  2128. } catch (error) {
  2129. console.error(`Error loading tile at ${x},${y}:`, error);
  2130. resolveTile();
  2131. }
  2132. });
  2133. };
  2134.  
  2135. let tilePromises = [];
  2136. for (let y = 0; y < tilesPerColumn; y++) {
  2137. for (let x = 0; x < tilesPerRow; x++) {
  2138. tilePromises.push(loadTile(x, y));
  2139. }
  2140. }
  2141.  
  2142. await Promise.all(tilePromises);
  2143.  
  2144. canvas.toBlob(blob => {
  2145. const url = window.URL.createObjectURL(blob);
  2146. const a = document.createElement('a');
  2147. a.href = url;
  2148. a.download = fileName;
  2149. document.body.appendChild(a);
  2150. a.click();
  2151. document.body.removeChild(a);
  2152. window.URL.revokeObjectURL(url);
  2153. resolve();
  2154. }, 'image/jpeg');
  2155. } catch (error) {
  2156. Swal.fire({
  2157. title: 'Error!',
  2158. text: error.toString(),
  2159. icon: 'error',
  2160. backdrop: false
  2161. });
  2162. reject(error);
  2163. }
  2164. });
  2165. }
  2166.  
  2167. async function loadImage(url) {
  2168. return new Promise((resolve, reject) => {
  2169. const img = new Image();
  2170. img.crossOrigin = 'Anonymous';
  2171. img.onload = () => resolve(img);
  2172. img.onerror = () => reject(new Error(`Failed to load image from ${url}`));
  2173. img.src = url;
  2174. });
  2175. }
  2176.  
  2177. window.addEventListener('popstate', function(event) {
  2178. const container = document.getElementById('coordinates-container');
  2179. if (container) {
  2180. container.remove();
  2181. }
  2182. });
  2183.  
  2184. XMLHttpRequest.prototype.realOpen = XMLHttpRequest.prototype.open;
  2185. XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
  2186. this._url = url;
  2187. this.realOpen(method, url, async, user, pass);
  2188. };
  2189.  
  2190. let onKeyDown =async (e) => {
  2191. if (e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) {
  2192. return;
  2193. }
  2194. if (e.key === 'r' || e.key === 'R') {
  2195. e.stopImmediatePropagation();
  2196. localStorage.removeItem('address_source')
  2197. localStorage.removeItem('api_key')
  2198. Swal.fire('清除成功','获取地址信息的来源已重置,您的API密钥已从缓存中清除,请刷新页面后重新选择。','success');
  2199. }
  2200. else if (e.key === 'm' || e.key === 'M') {
  2201. e.stopImmediatePropagation();
  2202. if(!streetViewPanorama)getSvContainer()
  2203. if (isMapDisplay){
  2204. guideMap.style.display='none'
  2205. isMapDisplay=false
  2206. }
  2207. else{
  2208. guideMap.style.display='block'
  2209. isMapDisplay=true
  2210. }
  2211. }
  2212. else if(e.ctrlKey&&(e.key=='i'||e.key=='I')){
  2213. if(!streetViewPanorama)getSvContainer()
  2214. const allElements = document.querySelectorAll('*');
  2215. mapButton.click()
  2216. streetViewPanorama.setLinks([])
  2217. allElements.forEach(element => {
  2218. if (element.id === 'panels'||
  2219. element.type === 'button'||
  2220. element.classList.contains('gm-compass') ||
  2221. element.classList.contains('verson___kI92b') ||
  2222. element.classList.contains('navigate___xl6aN')
  2223. )element.style.display = 'none';
  2224. });
  2225. }
  2226. else if (e.key === 'x' || e.key === 'X') {
  2227. e.stopImmediatePropagation();
  2228. if(!streetViewPanorama)getSvContainer()
  2229. if(globalLat&&globalLng&&globalTimestamp){
  2230. const sunPosition=SunCalc.getPosition(globalTimestamp,globalLat, globalLng)
  2231. const altitude = sunPosition.altitude;
  2232. const azimuth = sunPosition.azimuth;
  2233. const altitudeDegrees = altitude * (180 / Math.PI);
  2234. const azimuthDegrees = azimuth * (180 / Math.PI);
  2235. streetViewPanorama.setPov({heading:azimuthDegrees+180,pitch:altitudeDegrees})
  2236. streetViewPanorama.setZoom(1)
  2237. }
  2238. }
  2239. else if ((e.ctrlKey )&&(e.key === 'v' || e.key === 'V')){
  2240. navigator.clipboard.readText().then(function(text) {
  2241. if(svType=='qq'&&text.length!=23)return
  2242. else if(svType=='baidu'&&text.length!=27) return
  2243. else if(svType=='google'&&![64,44,22].includes(text.length)) return
  2244. if(text.length==44)text=b64Enode(text)
  2245. previousPin=null
  2246. isJump=true
  2247. if(!streetViewPanorama)getSvContainer()
  2248. streetViewPanorama.setPano(text)
  2249. globalPanoId=streetViewPanorama.pano
  2250. }).catch(function(err) {
  2251. console.error('读取剪贴板失败: ', err);
  2252. });
  2253. }
  2254.  
  2255. else if (e.key === 'g' || e.key === 'G') {
  2256. e.stopImmediatePropagation();
  2257. if(!streetViewPanorama)getSvContainer()
  2258. if(globalLat&&globalLng&&globalTimestamp){
  2259. const moonPosition=SunCalc.getMoonPosition(globalTimestamp,globalLat, globalLng)
  2260. const altitude=moonPosition.altitude
  2261. const azimuth = moonPosition.azimuth;
  2262. const altitudeDegrees = altitude * (180 / Math.PI);
  2263. const azimuthDegrees = azimuth * (180 / Math.PI);
  2264. streetViewPanorama.setPov({heading:azimuthDegrees+180,pitch:altitudeDegrees})
  2265. streetViewPanorama.setZoom(1)
  2266. }
  2267. }
  2268. }
  2269.  
  2270. document.addEventListener("keydown", onKeyDown);
  2271. })();