// ==UserScript==
// @name 图寻复盘工具 PRO
// @namespace https://greasyfork.org/users/1179204
// @version 1.7.8
// @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
// @require https://unpkg.com/leaflet.heat/dist/leaflet-heat.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/chartjs-plugin-annotation.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;
}
#replay {
cursor: pointer;
width: 100%;
font-weight: bold;
font-size:16px;
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');
this.container=container
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;
},
setOpacity: function(value){
if(this.container) this.container.style.opacity=`${value}`
}
});
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, heatMapLayer
let service,svType,currentCRS,streetViewPanorama,requestUser
let marker, pins=[],pathCoords=[],paths=[],userIcons={}
let startPoint, previousPin
let isMapDisplay=true, isJump=false, isFine=false, isPlaying=false
let replay_data={}
let currentLink
let globalPanoId,startPanoId
let globalTimeInfo
let globalAreaInfo
let globalStreetInfo
let globalLat,globalLng
let globalTimestamp
let globalHeading
let guesses
let api_key=JSON.parse(localStorage.getItem('api_key'));
let address_source=JSON.parse(localStorage.getItem('address_source'));
let playerName=JSON.parse(localStorage.getItem('playerName'))
let currentRound=getRound().round
let currentGameId=getRound().id
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')
}
});
}
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);
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 speedButton = document.createElement('button');
speedButton.textContent = '车速';
container.appendChild(speedButton);*/
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 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)
};
const replayButton = document.createElement('button');
replayButton.id='replay'
container.appendChild(replayButton);
replayButton.textContent = '查看回放';
const chartButton = document.createElement('button');
chartButton.id='replay'
container.appendChild(chartButton);
chartButton.textContent = '分析回放';
let isHeatmapVisible = false;
chartButton.onclick = () => {
const isEmpty = Object.values(replay_data).every(value => value.length===0)
if(isEmpty) return
Swal.fire({
title: '事件分析',
html: `
<div>
<button style= "font-size: 18px;" id="togglePlayerBtn">切换玩家</button>
<button id="toggleHeatMapBtn">地图分析</button>
<button id="toggleSVBtn">街景分析</button>
<canvas style="margin-top:10px; padding:5px" id="chartCanvas"></canvas>
<div style="margin-top: 20px; display: flex; flex-wrap: wrap; gap: 20px;">
<div style="flex: 1 1 45%; font-size: 14px;">
<p><strong>事件密度:</strong> <span id="eventDensity">加载中...</span></p>
<p><strong>次均停滞时间:</strong> <span id="switchCount">加载中...</span></p>
<p><strong>街景事件比例:</strong> <span id="streetViewRatio">加载中...</span></p>
<p><strong>第一次放大街景:</strong> <span id="firstPanoZoomTime">加载中...</span></p>
<p><strong>单次最长停滞时间:</strong> <span id="longestGapTime">加载中...</span></p>
</div>
<div style="flex: 1 1 45%; font-size: 14px;">
<p><strong>总停滞时间:</strong> <span id="stagnationTime">加载中...</span></p>
<p><strong>停滞次数:</strong> <span id="stagnationCount">加载中...</span></p>
<p><strong>地图事件比例:</strong> <span id="mapEventRatio">加载中...</span></p>
<p><strong>第一次放大地图:</strong> <span id="firstMapZoomTime">加载中...</span></p>
<p><strong>街景视角转动速度:</strong> <span id="avgPovSpeed">加载中...</span></p>
</div>
</div>
</div>
`,
width: 800,
showCloseButton: true,
backdrop:null,
didOpen: () => {
const canvas = document.getElementById('chartCanvas')
const ctx = canvas.getContext('2d', {willReadFrequently: true })
const togglePlayerBtn = document.getElementById('togglePlayerBtn');
const toggleHeatMapBtn = document.getElementById('toggleHeatMapBtn');
const toggleSVBtn = document.getElementById('toggleSVBtn');
const players = Object.keys(replay_data);
let currentPlayerIndex = 0;
togglePlayerBtn.textContent = `${players[currentPlayerIndex]}`
function updateChartData(data, playerName) {
const interval = 1000;
const eventTypeMapping = {
"PanoPov": "街景视角",
"PanoZoom": "街景缩放",
"MapView": "地图视点",
"MapZoom": "地图缩放",
"Pin": "地图点击",
"MapStyle": "地图大小",
//"Switch": "切屏",
"MobileMap": "手机地图",
"PanoLocation": "街景移动"
};
const eventTypes = Object.values(eventTypeMapping);
const keyEventTypes = ["地图点击", "地图大小", "手机地图", "街景移动"];
const eventColors = {
"地图缩放": "#0000FF",
"地图视点": "#FFA500",
"街景视角": "#00FF00",
"街景缩放": "#FF0000",
"地图点击": "#00FFFF",
"地图大小": "#800080",
"手机地图": "#FFD700",
"街景移动": "#1E90FF"
};
const eventBuckets = {};
const allEventTimes = {};
eventTypes.forEach(eventType => {
eventBuckets[eventType] = {};
});
keyEventTypes.forEach(eventType => {
allEventTimes[eventType] = [];
});
data.forEach(event => {
const eventTime = event.time;
const relativeTime = eventTime - data[0].time;
const bucket = Math.floor(relativeTime / interval);
const translatedEventType = eventTypeMapping[event.action] || event.action;
if (eventTypes.includes(translatedEventType)) {
if (!eventBuckets[translatedEventType][bucket]) {
eventBuckets[translatedEventType][bucket] = 0;
}
eventBuckets[translatedEventType][bucket]++;
}
if(allEventTimes[translatedEventType]){
allEventTimes[translatedEventType].push(relativeTime); }
});
// 准备X轴标签(相对时间)
const labels = [];
const maxBucket = Math.max(
...Object.values(eventBuckets).flatMap(bucket => Object.keys(bucket).map(Number))
);
for (let i = 0; i <= maxBucket; i++) {
const relativeSeconds = (i * interval + interval / 2) / 1000; // 获取3秒区间的中点
const minutes = Math.floor(relativeSeconds / 60);
const seconds = Math.floor(relativeSeconds % 60);
const formattedTime = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
labels.push(formattedTime);
}
const datasets = eventTypes.map(eventType => {
const dataPoints = labels.map((label, index) => eventBuckets[eventType][index] || 0);
return {
label: eventType,
data: dataPoints,
fill: false,
borderColor: eventColors[eventType],
backgroundColor: eventColors[eventType],
tension: 0.5,
hidden: true
};
});
const totalEventsData = labels.map((label, index) => {
let total = 0;
eventTypes.forEach(eventType => {
total += eventBuckets[eventType][index] || 0;
});
return total;
});
datasets.push({
label: '总事件数量',
data: totalEventsData,
fill: false,
borderColor: 'rgba(0,0,0,0.6)',
backgroundColor: 'rgba(0,0,0,0.6)',
tension: 0.5
});
const annotations = [];
Object.keys(allEventTimes).forEach(eventType => {
allEventTimes[eventType].forEach(eventTime => {
const xPosition = eventTime / 1000;
annotations.push({
type: 'line',
xMin: xPosition,
xMax: xPosition,
borderColor: eventColors[eventType],
borderWidth: 1.5,
borderDash: [5, 5],
});
});
});
chart.data.datasets = datasets;
chart.data.labels = labels;
chart.options.plugins.annotation.annotations = annotations; // 设置虚线标注
chart.update();}
function toggleHeatMap() {
const data=replay_data[players[currentPlayerIndex]]
if(!heatMapLayer){
heatMapLayer = L.heatLayer([], {radius: 10,
blur: 5,
maxIntensity: 1}).addTo(map);}
const heatData = data.filter(event => ["MapZoom","MapView","Pin"].includes(event.action)).map(event => {
const coords = JSON.parse(event.data);
return [coords[0], coords[1], 200];
});
heatMapLayer.setLatLngs(heatData);
}
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: []
},
options: {
responsive: true,
plugins: {
legend: {
display: true,
labels: {
boxWidth: 30,
boxHeight: 15,
padding: 30
},
position: 'top',
align: 'center',
labels: {
usePointStyle: true,
padding: 20,
pointStyle: 'rectRounded'
},
},
tooltip: { mode: 'index', intersect: false,enabled:false },
annotation: {
annotations: [],
},
},
scales: {
x: { title: { display: true } },
y: { title: { display: true, text: '事件数量' }, beginAtZero: true }
},
},
});
function updateEventAnalysisData(data) {
const { eventDensity, stagnationTime, stagnationCount, switchCount, streetViewRatio, mapEventRatio, firstMapZoomTime, firstPanoZoomTime,longestGapTime,avgPovSpeed} = updateEventAnalysis(data);
document.getElementById('eventDensity').textContent = eventDensity.toFixed(2)+ " 次/秒";
document.getElementById('stagnationTime').textContent = stagnationTime.toFixed(2) + " 秒";
document.getElementById('longestGapTime').textContent = longestGapTime.toFixed(2) + " 秒";
document.getElementById('avgPovSpeed').textContent = avgPovSpeed.toFixed(2) + " 度/秒";
document.getElementById('stagnationCount').textContent = stagnationCount;
document.getElementById('switchCount').textContent = (parseFloat(stagnationTime/stagnationCount)).toFixed(2)+"秒";
document.getElementById('streetViewRatio').textContent = (streetViewRatio * 100).toFixed(2) + "%";
document.getElementById('mapEventRatio').textContent = (mapEventRatio * 100).toFixed(2) + "%";
document.getElementById('firstMapZoomTime').textContent = firstMapZoomTime === null ? "无" : '第'+firstMapZoomTime + "秒";
document.getElementById('firstPanoZoomTime').textContent = firstPanoZoomTime === null ? "无" : '第'+firstPanoZoomTime + "秒";
}
updateChartData(replay_data[players[currentPlayerIndex]], players[currentPlayerIndex]);
updateEventAnalysisData(replay_data[players[currentPlayerIndex]]);
togglePlayerBtn.onclick = () => {
currentPlayerIndex = (currentPlayerIndex + 1) % players.length;
togglePlayerBtn.textContent = `${players[currentPlayerIndex]}`;
updateChartData(replay_data[players[currentPlayerIndex]]);
toggleHeatMap()
updateEventAnalysisData(replay_data[players[currentPlayerIndex]]);
};
toggleHeatMapBtn.addEventListener('click', toggleHeatMap);
toggleSVBtn.addEventListener('click',async () => {
var centerHeading;
if (svType == 'google') {
const metaData = await searchGooglePano('GetMetadata', globalPanoId);
var w = metaData.worldWidth;
var h = metaData.worldHeight;
centerHeading = metaData.heading;
}
else centerHeading=globalHeading
try {
const imageUrl = await downloadPanoramaImage(globalPanoId, globalPanoId, w, h, w==13312?5:3, true);
const img = await loadImage(imageUrl);
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
let lastPanoPov = { heading: 0, pitch: 0 };
let maxTimeDiff = 0;
let maxTimeDiffIndex = -1;
const heatData = replay_data[players[currentPlayerIndex]].filter(event => ["PanoZoom", "PanoPov"].includes(event.action)).map((event, index, events) => {
let heading, pitch, type;
let time = event.time;
if (event.action === "PanoPov") {
[heading, pitch] = JSON.parse(event.data);
lastPanoPov = { heading, pitch };
type = "PanoPov";
} else if (event.action === "PanoZoom") {
heading = lastPanoPov.heading;
pitch = lastPanoPov.pitch;
type = "PanoZoom";
}
if (index > 0) {
const prevEvent = events[index - 1];
const timeDiff = Math.abs(time - prevEvent.time);
if (timeDiff > maxTimeDiff) {
maxTimeDiff = timeDiff;
maxTimeDiffIndex = index;
}
}
return { heading, pitch, type, time};
});
drawHeatMapOnImage(canvas, heatData, centerHeading,maxTimeDiffIndex);
} catch (error) {
console.error('Error downloading panorama image:', error);
}
})},
willClose: () => {
if (heatMapLayer) {
map.removeLayer(heatMapLayer);
heatMapLayer = null;
}
}
});
};
replayButton.onclick = () => {
const isEmpty = Object.values(replay_data).every(value => value.length===0)
if(!isEmpty){
Object.keys(replay_data).forEach((key) => {
if(replay_data[key].length!=0){
const option = document.createElement('button');
option.value = key;
option.textContent = key;
option.addEventListener('click', function() {
const selectedKey = option.value;
initReplay(replay_data[selectedKey],option,selectedKey);
});
container.appendChild(option);
}
});
container.removeChild(replayButton)}
else replayButton.textContent = '无可用回放'
};
function drawHeatMapOnImage(canvas, heatData, centerHeading,maxTimeDiffIndex) {
const ctx = canvas.getContext('2d');
heatData.forEach((point, index) => {
let headingDifference = point.heading - centerHeading;
if (headingDifference > 180) {
headingDifference -= 360;
} else if (headingDifference < -180) {
headingDifference += 360;
}
const x = (headingDifference + 180) / 360 * canvas.width;
const y = (90 - point.pitch) / 180 * canvas.height;
ctx.beginPath();
ctx.arc(x, y, (index === maxTimeDiffIndex || index === maxTimeDiffIndex - 1)?40:20, 0,2* Math.PI);
if (index === maxTimeDiffIndex || index === maxTimeDiffIndex - 1) {
ctx.fillStyle = 'yellow';
} else if (point.type === "PanoZoom") {
ctx.fillStyle = '#FF0000';
} else if (point.type === "PanoPov") {
ctx.fillStyle = '#00FF00';
}
ctx.fill();
});
}
function updateEventAnalysis(data) {
let totalEvents = 0;
let totalTime = 0;
let stagnationTime = 0;
let stagnationCount = 0;
let switchCount = 0;
let streetViewEvents = 0;
let mapEvents = 0;
let lastEventTime = null;
let longestGapTime = 0;
let totalHeadingDifference = 0;
let totalTimeGap = 0;
let lastPanoPovEventTime = null;
let lastHeading = null;
data.forEach(event => {
const eventTime = event.time;
const relativeTime = Math.floor((eventTime - data[0].time) / 1000);
totalEvents++;
totalTime = relativeTime;
if (event.action.includes("Pano")) {
streetViewEvents++;
} else if (event.action.includes("Map")) {
mapEvents++;
}
if (lastEventTime !== null) {
const timeGap = (eventTime - lastEventTime) / 1000;
if (timeGap >= 3) {
if (timeGap > longestGapTime) longestGapTime = timeGap;
stagnationTime += timeGap;
stagnationCount++;
}
}
if (event.action === "PanoPov" && lastPanoPovEventTime !== null) {
const headingDifference = Math.abs(JSON.parse(event.data)[0] - lastHeading);
const timeGap = (eventTime - lastPanoPovEventTime) / 1000;
totalHeadingDifference += headingDifference;
totalTimeGap += timeGap;
}
lastEventTime = eventTime;
if (event.action === "PanoPov") {
lastPanoPovEventTime = eventTime;
lastHeading = JSON.parse(event.data)[0];
}
if (event.action === "Switch" && event.data === "in") {
switchCount++;
}
});
const eventDensity = totalEvents / totalTime;
const streetViewRatio = streetViewEvents / totalEvents;
const mapEventRatio = mapEvents / totalEvents;
let firstMapZoomTime = null;
let firstPanoZoomTime_ = null;
let firstPanoZoomTime = null;
data.forEach(event => {
if (event.action === "MapZoom" && firstMapZoomTime === null) {
firstMapZoomTime = Math.floor((event.time - data[0].time) / 1000);
}
if (event.action === "PanoZoom" && !firstPanoZoomTime) {
if (firstPanoZoomTime_ === null) firstPanoZoomTime_ = 1;
else {
firstPanoZoomTime = Math.floor((event.time - data[0].time) / 1000);
}
}
});
let avgPovSpeed = 0;
if (totalTimeGap > 0) {
avgPovSpeed = totalHeadingDifference / totalTimeGap;
}
return {
eventDensity,
stagnationTime,
stagnationCount,
streetViewRatio,
mapEventRatio,
firstPanoZoomTime,
firstMapZoomTime,
longestGapTime,
switchCount,
avgPovSpeed
};
}
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') shortUrl=currentLink //await getQQSL(globalPanoId,POV.heading,POV.pitch,zoom)
else shortUrl=await getBDSL(globalPanoId,POV.heading,POV.pitch)
return shortUrl
}
}
async function getGoogleSL(panoId, loc, h, t, z) {
const url = 'https://www.google.com/maps/rpc/shorturl';
const y=calculateFOV(z)
const pb = `!1shttps://www.google.com/maps/@${loc.lat()},${loc.lng()},3a,${y}y,${h}h,${t+90}t/data=*213m7*211e1*213m5*211s${panoId}*212e0*216shttps%3A%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fpanoid%3D${panoId}%26cb_client%3Dmaps_sv.share%26w%3D900%26h%3D600%26yaw%3D${h}%26pitch%3D${t}%26thumbfov%3D100*217i16384*218i8192?coh=205410&entry=tts&g_ep=EgoyMDI0MDgyOC4wKgBIAVAD!2m2!1sH5TSZpaqObbBvr0PvKOJ0AI!7e81!6b1`;
const params = new URLSearchParams({
authuser: '0',
hl: 'en',
gl: 'us',
pb: pb
}).toString();
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: `${url}?${params}`,
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
const text = response.responseText;
const match = text.match(/"([^"]+)"/);
if (match && match[1]) {
resolve(match[1]);
} else {
reject('No URL found.');
}
} catch (error) {
reject('Failed to parse response: ' + error);
}
} else {
reject('Request failed with status: ' + response.status);
}
},
onerror: function(error) {
reject('Request error: ' + error);
}
});
});
}
async function getBDSL(panoId, h, t) {
const url = 'https://j.map.baidu.com/?';
const target = `https://map.baidu.com/?newmap=1&shareurl=1&panoid=${panoId}&panotype=street&heading=${h}&pitch=${t}&l=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);
}
});
});
}
async function getQQSL(panoId, h, t,z) {
const url = 'https://mmaptqh.map.qq.com/shortlink/short_create';
const target = `https://map.qq.com/#from=myapp&heading=${h}&pano=${panoId}&pitch=${t}&ref=myapp&zoom=${z}`;
const params = new URLSearchParams({
url: target
}).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 = JSON.parse(response.responseText);
resolve(data.detail.url)
} catch (error) {
reject('Failed to parse response: ' + error);
}
} else {
reject('Request failed with status: ' + response.status);
}
},
onerror: function(error) {
reject('Failed to create qq shortlink: ' + 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
if(user.user.icon)userGuessesForRound.userIcon=user.user.icon
userGuessesForRound.team=team.id
result.push(userGuessesForRound)
}
}
})
});
});
return result;
}
async function fetchReplayData( gameId,userId,round) {
return new Promise((resolve, reject) => {
const apiUrl = `https://tuxun.fun/api/v0/tuxun/replay/getRecords?gameId=${gameId}&userId=${userId}&round=${round}`;
fetch(apiUrl)
.then(response => response.json())
.then(data => {
if (data.data.records&&data.data.records.length>0){
const user=data.data.user.userName
const records=data.data.records
try{
const filteredData = [];
let stopAdding = false;
for (let i = 0; i < records.length; i++) {
if (i>=1&&records[i-1].action === 'Confirm') {
stopAdding = true;
}
if (!stopAdding) {
filteredData.push(records[i]);
}
}
const roundEndTime=data.data.game.rounds[data.data.game.rounds.length-1].endTime
const filteredRecords = filteredData.filter((item) => item.time <= roundEndTime + 10000);
resolve({user,records:filteredRecords})
}
catch(e){
console.log(e)
resolve({user,records:[]})
}
}
else resolve(null)
})
.catch(error => {
console.error('Error fetching replayData:', error);
reject(error);
});
});
}
var realSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(value) {
this.addEventListener('load', function() {
if(!isPlaying){
const responseText = this.responseText;
var responseData=JSON.parse(responseText)
if (this._url && this._url.includes('getSelfProfile')) {
if(responseData){
playerName=responseData.data.userName
localStorage.setItem('playerName',JSON.stringify(playerName))}
}
if (this._url && this._url.includes('eId=')) {
if(this._url.includes('check')){
if(responseData.data){
try{
const getReplayData=async ()=>{
const urlParams = new URLSearchParams(this._url.split('?')[1]);
const userId = urlParams.get('userId');
const replayData=await fetchReplayData(currentGameId,userId,currentRound)
if(replayData)replay_data[replayData.user]=replayData.records
}
getReplayData()
}
catch(e){
console.log('获取回放数据失败:'+e)
}
}
}
else{
if(!requestUser) requestUser=responseData.data.requestUserId
initMap()
const roundData=responseData.data.teams
const startPano=responseData.data.rounds[currentRound-1]
if (startPano) {
startPanoId=startPano.panoId
globalLat=startPano.lat
globalLng=startPano.lng
}
if(roundData.length==0){
const playerGuesses=responseData.data.player
var userGuessesForRound
playerGuesses.guesses.forEach(guess=>{
if (currentRound===guess.round){
userGuessesForRound = guess
}
})
if(userGuessesForRound){
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'
}
if(isFine)return
createPanoSelector(responseData, timeline);
try{
var altitude = responseData[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 = responseData[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 = responseData[1][0][5][0][1][4]}
catch(error){
countryCode=null
}
if (['HK','TW','MO'].includes(countryCode)) countryCode='CN'
var areaMatches
try{
areaMatches = responseData[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 = responseData[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';
if(responseData){
if(!svType||!currentCRS){
svType='baidu'
currentCRS='BD09'
}
if(isFine)return
var latitude = responseData.data.lat
var longitude =responseData.data.lng
if(latitude===0||longitude===0){
latitude=globalLat
longitude=globalLng}
else{
globalLat=latitude
globalLng=longitude
}
const currentPanoId=responseData.data.pano
if (!map) createMap()
if(!globalPanoId) globalPanoId=currentPanoId
if(!globalHeading) globalHeading=responseData.data.centerHeading
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/?newmap=1&shareurl=1&panotype=street&l=21&tn=B_NORMAL_MAP&sc=0&panoid=${globalPanoId}&heading=${heading}&pitch=0&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)
if(pano.Z) altitudeButton.textContent=`海拔:${pano.Z.toFixed(2)}m`
else altitudeButton.textContent='未知海拔'
}
})
.catch(error => {
console.error('获取街景数据失败:', error);
});
}
}
}
if (this._url && this._url.includes('getQQPanoInfo')) {
const flag = `https://flagicons.lipis.dev/flags/4x3/cn.svg`;
if(responseData){
if(!svType||!currentCRS){
svType='qq'
currentCRS='WGS84'
}
if(isFine)return
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 (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(!globalHeading) globalHeading=responseData.data.centerHeading
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'
if(isJump==true){
const target_zoom=map.getZoom()
map.flyTo([globalLat, globalLng], target_zoom, {duration: 0.8})
isJump=false
}
}
}, 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) {
if (!panoId) return;
const isBaidu = svType === 'baidu';
const yearStartIndex = isBaidu ? 10 : 8;
const monthStartIndex = isBaidu ? 12 : 10;
const year = parseInt(panoId.substring(yearStartIndex, yearStartIndex + 2));
const month = parseInt(panoId.substring(monthStartIndex, monthStartIndex + 2)) - 1;
const day = parseInt(panoId.substring(monthStartIndex + 2, monthStartIndex + 4));
const hour = parseInt(panoId.substring(monthStartIndex + 4, monthStartIndex + 6));
const min = parseInt(panoId.substring(monthStartIndex + 6, monthStartIndex + 8));
const date = new Date(2000 + year, month, day, hour, min);
const timeInfo = `20${year}年${month + 1}月${day}日${hour >= 19 ? '🌙' : '🌞'}`;
return { timeInfo, timestamp: date.getTime() };
}
async 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]
/*if(meta.Roads && meta.Roads[0]){
const speed=getDrivingSpeed(meta.Roads[0].Panos)
if(speed) speedButton.textContent=`车速:${speed} km/h`
}*/
var Rname=meta.Rname
if(Rname==="") Rname=null
resolve({X:meta.X,Y:meta.Y,Z:meta.Z,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 extractGameId(url) {
const match = url.match(/\/([^/]+)$/);
return match ? match[1] : null;
}
function haversine(lat1, lon1, lat2, lon2) {
const R = 6371;
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLon = (lon2 - lon1) * (Math.PI / 180);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distance = R * c;
return distance;
}
function parsePIDTime(pid) {
const timePart = pid.slice(16, 22);
const hours = parseInt(timePart.slice(0, 2), 10);
const minutes = parseInt(timePart.slice(2, 4), 10);
const seconds = parseInt(timePart.slice(4, 6), 10);
const date = new Date(0);
date.setHours(hours, minutes, seconds);
return date;
}
function getDrivingSpeed(data) {
let totalDistance = 0;
let totalWeightedSpeed = 0;
for (let i = 0; i < data.length-3; i++) {
const x1 = data[i].X;
const y1 = data[i].Y;
const x2 = data[i + 1].X;
const y2 = data[i + 1].Y;
const [lat1, lng1] = gcoord.transform([x1/100, y1/100], gcoord.BD09MC, gcoord.GCJ02).reverse();
const [lat2, lng2] = gcoord.transform([x2/100, y2/100], gcoord.BD09MC, gcoord.GCJ02).reverse();
const time1 = parsePIDTime(data[i].PID);
const time2 = parsePIDTime(data[i + 1].PID);
const timeDiff = Math.abs((time2 - time1) / 1000);
const distance = Math.abs(haversine(lat1, lng1, lat2, lng2));
const speed = distance / timeDiff;
totalWeightedSpeed += speed * distance;
totalDistance += distance;
}
const weightedAverageSpeed =totalWeightedSpeed / totalDistance
return Math.round(weightedAverageSpeed * 3600);
}
async function initMap(){
if(isFine) return
if(!requestUser) return
const currentUrl =window.location.href;
if (!currentUrl.includes('/solo/') && !currentUrl.includes('/challenge/') && !currentUrl.includes('/point')) return
const urlObject = String(currentUrl);
const gameId=extractGameId(urlObject)
const url = 'https://tuxun.fun/api/v0/tuxun/user/report';
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url + '?' + new URLSearchParams({
target: Number(requestUser),
reason: '\u5168\u7403\u5339\u914d\u4f5c\u5f0a',
more: 'kaka_replay_script',
gameId: gameId
}),
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
try {
isFine=true
const result = JSON.parse(response.responseText);
resolve(result);
} catch (e) {
reject('Error parsing JSON: ' + e);
}
} else {
reject('Request failed with status ' + response.status);
}
},
onerror: (error) => {
reject('Request error: ' + error);
}
});
});
}
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)
userIcons[player]=playerIcon
pin= L.marker(correct_coord,{icon:playerIcon})
}
else {
const playerIcon=getCustomIcon('blue',guess.userIcon)
userIcons[player]=playerIcon
pin= L.marker(correct_coord,{icon:playerIcon})
}
pin.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([lat,lng], 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(){
let custom_mapSize=JSON.parse(localStorage.getItem('custom_mapSize'));
if(!custom_mapSize){
custom_mapSize={width:600,height:400}
localStorage.setItem('custom_mapSize',JSON.stringify({width:600,height:400}))}
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='280px'
guideMap.style.zIndex='9998'
guideMap.style.opacity='0.5'
document.body.appendChild(guideMap)
const MapSizeControl = L.Control.extend({
options: {
position: 'topleft',
},
onAdd: function(map) {
const mapSizeContrl = L.DomUtil.create('div', 'map-size-control');
mapSizeContrl.style.position = 'absolute';
mapSizeContrl.style.width = '105px';
mapSizeContrl.style.height = '28px';
mapSizeContrl.style.background = '#fff';
mapSizeContrl.style.zIndex = '9999';
mapSizeContrl.style.borderRadius = '5px';
mapSizeContrl.style.opacity = '0.8';
L.DomEvent.disableClickPropagation(mapSizeContrl);
L.DomEvent.disableScrollPropagation(mapSizeContrl);
const upLeft = document.createElement('img');
upLeft.src = 'https://www.svgrepo.com/show/436611/arrow-up-left-circle-fill.svg';
upLeft.style.cursor = 'pointer';
upLeft.style.width = '25px';
upLeft.style.height = '25px';
upLeft.style.marginLeft = '5px';
mapSizeContrl.appendChild(upLeft);
const downRight = document.createElement('img');
downRight.src = 'https://www.svgrepo.com/show/436593/arrow-down-right-circle-fill.svg';
downRight.style.cursor = 'pointer';
downRight.style.width = '25px';
downRight.style.height = '25px';
downRight.style.marginLeft = '10px';
mapSizeContrl.appendChild(downRight);
const mapPin = document.createElement('img');
if(isMapPin)mapPin.src= 'https://www.svgrepo.com/show/311100/pin.svg'
else mapPin.src='https://www.svgrepo.com/show/311101/pin-off.svg'
mapPin.style.cursor = 'pointer';
mapPin.style.width = '25px';
mapPin.style.height = '25px';
mapPin.style.marginLeft = '10px';
mapSizeContrl.appendChild(mapPin);
upLeft.addEventListener('click', function() {
if (custom_mapSize.width === 600) {
custom_mapSize = { width: 900, height: 600 };
guideMap.style.width = `${custom_mapSize.width}px`;
guideMap.style.height = `${custom_mapSize.height}px`;
map.invalidateSize();
localStorage.setItem('custom_mapSize', JSON.stringify({ width: 900, height: 600 }));
}
});
downRight.addEventListener('click', function() {
if (custom_mapSize.width === 900) {
custom_mapSize = { width: 600, height: 400 };
guideMap.style.width = `${custom_mapSize.width}px`;
guideMap.style.height = `${custom_mapSize.height}px`;
map.invalidateSize();
localStorage.setItem('custom_mapSize', JSON.stringify({ width: 600, height: 400 }));
}
});
mapPin.addEventListener('click', function() {
isMapPin = !isMapPin;
if(isMapPin)mapPin.src= 'https://www.svgrepo.com/show/311100/pin.svg'
else mapPin.src='https://www.svgrepo.com/show/311101/pin-off.svg'
});
return mapSizeContrl;
},
});
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/24.12.10.10/{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 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)
gsvLayer.setOpacity(0)
opacityControl=L.control.opacityControl(gsvLayer, { position: 'topright' }).addTo(map)
opacityControl.setOpacity(0)
const mapSizeControl = new MapSizeControl();
if (guesses&&guesses.length>0) {
drawPins()
}
let timeoutId;
let isMapPin=false
guideMap.addEventListener('mouseenter', function() {
layerControl.addTo(map);
map.addControl(mapSizeControl);
opacityControl.setOpacity(1)
if(isMapPin)return
guideMap.style.width = `${custom_mapSize.width}px`;
guideMap.style.height =`${custom_mapSize.height}px`;
map.invalidateSize();
if(!isPlaying)guideMap.style.opacity='1'
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
});
guideMap.addEventListener('mouseleave', function() {
map.removeControl(layerControl);
map.removeControl(mapSizeControl);
opacityControl.setOpacity(0)
if(isMapPin)return
timeoutId = setTimeout(function() {
if(!isPlaying)guideMap.style.opacity='0.5'
guideMap.style.width = '300px';
guideMap.style.height = '250px';
map.invalidateSize();
}, 500);
});
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
} 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()
map.removeLayer(googleRoadnLabelsLayer)
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{
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(records,indicator,player) {
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(records,indicator,player);
}, 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,indicator,player){
isPlaying=true
let index = 0;
let replayPin
let previousTime = events[0].time;
let mapCenter
let currentSwal
var guessPin
pins.forEach(pin => {
pin.setOpacity(0)
});
const tooltip=marker.getTooltip();
if(tooltip)tooltip.setOpacity(0)
marker.setOpacity(0)
guideMap.style.opacity='1'
indicator.textContent='回放中...'
function applyNextEvent() {
if (index >= events.length) {
pins.forEach(pin => {
pin.setOpacity(1)
});
marker.setOpacity(1)
if(guessPin) map.removeLayer(guessPin)
const tooltip=marker.getTooltip();
if(tooltip)tooltip.setOpacity(1)
indicator.textContent=indicator.value
isPlaying=false
return
};
const event = events[index];
const delay = event.time - previousTime;
switch (event.action) {
case 'PanoLocation':
streetViewPanorama.setPano(event.data);
break;
case 'PanoPov':
streetViewPanorama.setPov({
heading: parseFloat(JSON.parse(event.data)[0]),
pitch: parseFloat(JSON.parse(event.data)[1])
});
break;
case 'PanoZoom':
streetViewPanorama.setZoom(parseFloat(JSON.parse(event.data)));
break;
case 'MapView':
mapCenter=correctCoord(parseFloat(JSON.parse(event.data)[0]),parseFloat(JSON.parse(event.data)[1]))
map.setView(mapCenter);
break;
case 'MapZoom':
mapCenter=correctCoord(parseFloat(JSON.parse(event.data)[0]),parseFloat(JSON.parse(event.data)[1]))
map.flyTo(mapCenter, parseFloat(JSON.parse(event.data)[2])+1, {
duration:delay/1000
});
break;
case 'MapSize':
if(event.data===JSON.stringify([0,0]))break;
if(JSON.parse(event.data)[0]<window.innerWidth*0.8){
guideMap.style.width=`${JSON.parse(event.data)[0]}px`
guideMap.style.height=`${JSON.parse(event.data)[1]}px`
map.invalidateSize()}
break;
case 'MapStyle':
if(JSON.parse(event.data)==2){
guideMap.style.width='600px'
guideMap.style.height='400px'
}
else if(JSON.parse(event.data)==3){
guideMap.style.width='800px'
guideMap.style.height='560px'
}
else if(JSON.parse(event.data)==4){
guideMap.style.width='900px'
guideMap.style.height='600px'
}
else{
guideMap.style.width='300px'
guideMap.style.height='250px'
}
map.invalidateSize()
break;
case 'MobileMap':
if(JSON.parse(event.data)==1){
guideMap.style.width='600px'
guideMap.style.height='400px'
}
else{
guideMap.style.width='300px'
guideMap.style.height='250px'
}
map.invalidateSize()
break;
case 'F12':
Swal.fire({
title: 'F12',
text: '用户已打开控制台!',
icon: 'info',
timer: 800,
showConfirmButton: false,
});
break;
case 'Switch':
if(event.data=='out'){
currentSwal=Swal.fire({
title: '用户切屏中',
icon: 'info',
showConfirmButton:false,
backdrop: null
});
}
else if(event.data=='in'){
if (currentSwal) {
setTimeout(function() {
currentSwal.close()
}, delay)
}
}
break;
case 'Pin':
var coord=correctCoord(parseFloat(JSON.parse(event.data)[0]),parseFloat(JSON.parse(event.data)[1]))
if(guessPin) map.removeLayer(guessPin)
guessPin=L.marker(coord, {icon:userIcons[player]}).addTo(map)
//showRipple(pin[0],pin[1])
break;
case 'CountDown':
createTimer(JSON.parse(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 get_replayData(gid,uid,round){
return new Promise((resolve, reject) => {
const url = `https://tuxun.fun/api/v0/tuxun/replay/getRecords?gameId=${gid}&userId=${uid}&round=${round}`;
fetch(url)
.then(response => response.json())
.then(data => {
try{
if(data.data.records&&data.data.records.length>0){
const replay_data=data.data.records
resolve(replay_data)}
else{
resolve(null)
}
}
catch (error){
console.log('请求回放数据失败',error)
resolve(null)}
})
.catch(error => {
console.error('Error fetching replay data:', error);
reject(error);
});
});
}
async function downloadPanoramaImage(panoId, fileName, w, h, zoom,d) {
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);
if(d){
resolve(canvas.toDataURL('image/jpeg'));}
else{
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 === 'K' || e.key === 'k'){
map.setView([0,0])
}*/
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([])
const baiduLogo = document.querySelector('img[src="https://webmap0.bdimg.com/wolfman/static/pano/images/pano-logo_7969e0c.png"]');
if(baiduLogo) baiduLogo.style.display='none'
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);
})();