// ==UserScript==
// @name 图寻连击计数器
// @namespace https://greasyfork.org/users/1179204
// @version 1.1.7
// @description 自动记录国家/一级行政区连击次数
// @author KaKa
// @match *://tuxun.fun/*
// @exclude *://tuxun.fun/replay-pano?*
// @icon data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDggNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0iIzAwMDAwMCI+PGcgaWQ9IlNWR1JlcG9fYmdDYXJyaWVyIiBzdHJva2Utd2lkdGg9IjAiPjwvZz48ZyBpZD0iU1ZHUmVwb190cmFjZXJDYXJyaWVyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjwvZz48ZyBpZD0iU1ZHUmVwb19pY29uQ2FycmllciI+PHRpdGxlPjcwIEJhc2ljIGljb25zIGJ5IFhpY29ucy5jbzwvdGl0bGU+PHBhdGggZD0iTTI0LDEuMzJjLTkuOTIsMC0xOCw3LjgtMTgsMTcuMzhBMTYuODMsMTYuODMsMCwwLDAsOS41NywyOS4wOWwxMi44NCwxNi44YTIsMiwwLDAsMCwzLjE4LDBsMTIuODQtMTYuOEExNi44NCwxNi44NCwwLDAsMCw0MiwxOC43QzQyLDkuMTIsMzMuOTIsMS4zMiwyNCwxLjMyWiIgZmlsbD0iI2ZmOTQyNyI+PC9wYXRoPjxwYXRoIGQ9Ik0yNS4zNywxMi4xM2E3LDcsMCwxLDAsNS41LDUuNUE3LDcsMCwwLDAsMjUuMzcsMTIuMTNaIiBmaWxsPSIjZmZmZmZmIj48L3BhdGg+PC9nPjwvc3ZnPg==
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
// @require https://unpkg.com/gcoord/dist/gcoord.global.prod.js
// @grant GM_setClipboard
// @grant GM_xmlhttpRequest
// @license BSD
// ==/UserScript==
(function() {
const Language='zh' // ISO 639-1 语言代码 - https://baike.baidu.com/item/ISO%20639
let viewer,map,finalGuess,currentRound,gameState=false,roundPins={},gameMode,roundState,countsDiv,countsTitle,countsValue,mapsId,avgScore,avgValue_,previousWidth=0
let api_key=''//JSON.parse(localStorage.getItem('api_key'));
let streakCounts=JSON.parse(localStorage.getItem('streakCounts'))
let streakMode=JSON.parse(localStorage.getItem('streakMode'))
if (!streakCounts){
streakCounts={}
}
if (!streakMode){
streakMode='country'
}
const CC_DICT = {
AX: "FI", AS: "US", AI: "GB", AW: "NL", BM: "GB", BQ: "NL", BV: "NO", IO: "GB", KY: "UK",
CX: "AU", CC: "AU", CK: "NZ", CW: "NL", FK: "AR", FO: "DK", GF: "FR", PF: "FR", TF: "FR",
GI: "UK", GL: "DK", GP: "FR", GU: "US", GG: "GB", HM: "AU", HK: "CN", IM: "GB", JE: "GB",
MO: "CN", MQ: "FR", YT: "FR", MS: "GB", AN: "NL", NC: "FR", NU: "NZ", NF: "AU", MP: "US",
PS: "IL", PN: "GB", PR: "US", RE: "FR", BL: "FR", SH: "GB", MF: "FR", PM: "FR", SX: "NL",
GS: "GB", SJ: "NO", TK: "NZ", TC: "GB", UM: "US", VG: "GB", VI: "US", WF: "FR", EH: "MA",
TW: "CN"
};
let intervalId=setInterval(function(){
const streetViewContainer= document.getElementById('viewer')
if(streetViewContainer){
getSVContainer()
getMap()
if(map&&viewer&&viewer.location&&gameMode){
mapListener()
clearInterval(intervalId)}
}
},500);
function getMap(){
var mapContainer = document.getElementById('map')
const keys = Object.keys(mapContainer)
const key = keys.find(key => key.startsWith("__reactFiber$"))
const props = mapContainer[key]
const x = props.child.memoizedProps.value.map
map=x.getMap()
}
function getSVContainer(){
const streetViewContainer= document.getElementById('viewer')
const keys = Object.keys(streetViewContainer)
const key = keys.find(key => key.startsWith("__reactFiber"))
const props = streetViewContainer[key]
viewer=props.return.child.memoizedProps.children[1].props.googleMapInstance
const gameData=props.return.return.return.return.return.memoizedState.next.next.memoizedState.current.gameData
if(gameData){
if(gameData.status&&gameData.status==='ongoing'){
gameState=roundState=true
mapsId=gameData.mapsId
if (['challenge','infinity'].includes(gameData.type)) gameMode=gameData.type
if (!streakCounts[mapsId]){
streakCounts[mapsId]={'country':0,'state':0}
}
currentRound=gameData.rounds.length
if(gameData.rounds[currentRound-1].endTime) currentRound+=1
}
}
}
function mapListener(){
setMapObserver()
setSVObserver()
if (!roundPins[currentRound]){
getRoundPin()
updatePanel(streakMode)
}
var mapContainer = document.querySelector('.maplibregl-canvas')
const observer = new MutationObserver((mutationsList, observer) => {
for(let mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
handleSizeChange(mapContainer);
}
}
});
observer.observe(mapContainer, { attributes: true, attributeFilter: ['style'] });
}
function setMapObserver() {
map.on('click', (e) => {
if (gameState&&roundState) finalGuess=e.lngLat
});
}
function setSVObserver() {
viewer.addListener('position_changed', () => {
if (!roundPins[currentRound]&&gameState){
getRoundPin()
}
});
}
async function getRoundPin(){
var lat,lng,add
const panoId=viewer.getPano()
if(panoId.length===27) {
[lat,lng]=await checkBDPano(viewer.pano)
if(api_key) add=await queryGD(lat,lng)
else add=await queryOSM(lat,lng)
}
else if(panoId.length==23){
[lat,lng]=await checkQQPano(viewer.pano)
if(api_key) add=await queryGD(lat,lng)
else add=await queryOSM(lat,lng)
}
else{
lat=viewer.location.latLng.lat()
lng=viewer.location.latLng.lng()
if(api_key) add=await queryGD(lat,lng)
else add=await queryOSM(lat,lng)}
roundPins[currentRound]=add
}
function handleSizeChange(target) {
const { width, height } = target.getBoundingClientRect();
const currentScreenWidth = window.innerWidth;
const widthRatio = (width / currentScreenWidth) * 100;
if (widthRatio>=90&&previousWidth<90) {
streakCheck()
}
else {
roundState=true
updatePanel()
}
if(previousWidth!=widthRatio)previousWidth=widthRatio
}
async function queryOSM(lat, lng) {
const url =`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=jsonv2&accept-language=${Language}`;
const response = await fetch(url);
if (response.ok) {
let data = await response.json();
if(data.address) return data.address
} else {
return null;
}
}
async function streakCheck(){
if(!roundState) return
const panoId=viewer.getPano()
if(finalGuess){
var guess,answer
if(panoId.length===27) {
if(api_key) guess=await queryGD(finalGuess.lat,finalGuess.lng)
else guess=await queryOSM(finalGuess.lat,finalGuess.lng)
}
else {
if(api_key) guess=await queryGD(finalGuess.lat,finalGuess.lng)
else guess=await queryOSM(finalGuess.lat,finalGuess.lng)
}
answer=roundPins[currentRound]
var isStreak
if(streakMode==='country'){
if(panoId.length!=27){
if(matchCountryCode(guess)===matchCountryCode(answer)){
isStreak=true
}}
else{
if(guess&&(guess.country_code in ['tw','cn','mo','hk'])) isStreak=true
}
}
else if(streakMode==='state'){
if(matchState(guess)===matchState(answer)){
isStreak=true
}
else if (guess.country_code=='tw'&answer.country_code=='tw') isStreak=true
}
if(guess) updateBar(isStreak,guess,answer,streakMode)
else updateBar(false,'Undefined',answer,streakMode)
currentRound+=1
}
roundState=false
}
function correctCountry(item){
if(item==='Undefined') return item
try{
if(['Taiwan','HongKong','Macau','臺灣','台湾'].includes(item.country)) return Language=== 'zh' ? '中国' : 'China'
else if(item.country_code==='xk') return Language=== 'zh' ? '塞尔维亚' : 'Serbia'
else if(item['ISO3166-2-lvl4']==='IN-AR') return Language=== 'zh' ? '中国' : 'China'
else if(item.country.length===0) return 'Undefined'
else return item.country
}
catch (error){
console.error('failed to correct country')
return 'Undefined'
}
}
function correctState(item){
try{
if (item.country.length===0) return 'Undefined'
else if(item['ISO3166-2-lvl4']==='IN-AR')return Language=== 'zh' ? '西藏自治区': 'Tibet'
else if(item.country_code==='tw') return Language=== 'zh' ? '台湾省' : 'Taiwan Province'
else if(item['ISO3166-2-lvl4']==='JP-13') return Language=== 'zh' ?'东京都': 'Tokyo'
else if(item.country_code==='fk')return Language==='zh'?'福克兰群岛':'Falkland Islands'
else if(item.country_code==='pn')return Language==='zh'?'皮特凯恩群岛':'Pitcairn Islands'
else return matchState(item)
}
catch(error) {
console.error('failed to correct state')
return 'Undefined'
}
}
function updateBar(status,pin,result){
const roundBar=document.querySelector('.scoreReulstValue___gFyI2')
if (roundBar)roundBar.textContent=roundBar.textContent.split('/')[0]
const infoBar=document.querySelector('.controls___yY74y')
const pText=infoBar.querySelector('p')
if(pText) pText.style.display='none'
const streakText = document.createElement('div')
streakText.style.fontSize='24px'
streakText.style.color='#fff'
streakText.style.marginTop='15px'
streakText.style.fontFamily='Baloo Bhaina'
infoBar.appendChild(streakText)
if (infoBar) {
let message = '';
let answer = '';
let guess = '';
let streakMessage = '';
const correctTextColor = 'green';
const userTextColor = 'red';
const streakColor = 'yellow';
if (status) {
streakCounts[mapsId][streakMode] += 1;
if (streakMode === 'country') {
answer = correctCountry(result).split('/')[0];
message = `恭喜你选对 <span style="color: ${correctTextColor};">${answer}</span> , 连击次数: <span style="color: ${streakColor};">${streakCounts[mapsId][streakMode]}</span>`;
} else if (streakMode === 'state') {
answer = correctState(result).split('/')[0];
message = `恭喜你选对 <span style="color: ${correctTextColor};">${answer}</span> , 连击次数: <span style="color: ${streakColor};">${streakCounts[mapsId][streakMode]}</span>`;
}
} else {
const end_count = streakCounts[mapsId][streakMode];
streakCounts[mapsId][streakMode] = 0;
if (streakMode === 'country') {
answer = correctCountry(result).split('/')[0];
guess = correctCountry(pin).split('/')[0];
message = `答案是 <span style="color: ${correctTextColor};">${answer}</span> , 你选了 <span style="color: ${userTextColor};">${guess}</span> , 连击次数: <span style="color: ${streakColor};">${streakCounts[mapsId][streakMode]}</span> , 本轮达成连击: <span style="color: ${streakColor};">${end_count}</span>`;
} else if (streakMode === 'state') {
answer = correctState(result).split('/')[0];
guess = correctState(pin).split('/')[0];
message = `答案是 <span style="color: ${correctTextColor};">${answer}</span> , 你选了 <span style="color: ${userTextColor};">${guess}</span> , 连击次数: <span style="color: ${streakColor};">${streakCounts[mapsId][streakMode]}</span> , 本轮达成连击: <span style="color: ${streakColor};">${end_count}</span>`;
}
}
streakText.innerHTML = message;
localStorage.setItem('streakCounts', JSON.stringify(streakCounts));
}
const scoreBar=document.querySelector('.scoreReulst___qqkPH')
const scoresDiv=document.querySelectorAll('.scoreReulstValue___gFyI2')[3]
if(scoresDiv.textContent) avgScore=parseInt(scoresDiv.textContent.replace(',', ''))
}
async function queryGD(lat, lng) {
const apiUrl = `https://restapi.amap.com/v3/geocode/regeo?output=json&location=${lng},${lat}&key=${api_key}&radius=50`;
try {
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error('Request failed with status: ' + response.status);
}
const data = await response.json();
if (data.status === '1' && data.regeocode) {
return data.regeocode.addressComponent;
} else {
localStorage.removeItem('api_key');
Swal.fire('无效的API密钥', '请刷新页面并重新输入正确的高德地图API密钥', 'error');
throw new Error('Request failed: ' + data.info);
}
} catch (error) {
console.error('Error fetching address:', error);
throw error;
}
}
function checkBDPano(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.result.error !== 404) {
var lat,lng
if(Math.floor(Math.log10(data.content[0].X)) + 1<7) [lng,lat]= gcoord.transform([data.content[0].X, data.content[0].Y],gcoord.BD09MC,gcoord.WGS84)
else [lng,lat] = gcoord.transform([data.content[0].X/100, data.content[0].Y /100],gcoord.BD09MC, gcoord.WGS84)
resolve([lat,lng])
}
else {
resolve(false)
}
}
catch (error) {
resolve(false)
}
})
.catch(error => {
console.error('Request failed:', error);
reject(error);
});
});
}
function checkQQPano(id) {
return new Promise((resolve, reject) => {
const url = `https://sv.map.qq.com/sv?svid=${id}&output=json`;;
fetch(url)
.then(response => response.json())
.then(data => {
try {
if (data.detail) {
var pano=data.detail.addr
resolve([pano.y_lat,pano.x_lng])
}
else {
resolve(false)
}
}
catch (error) {
resolve(false)
}
})
.catch(error => {
console.error('Request failed:', error);
reject(error);
});
});
}
function updatePanel(){
const panel_container=document.querySelector('.roundWrapper___eTnOj ')
if(!countsDiv){
countsDiv=document.createElement('div')
countsDiv.className='roundInfoBox___ikizG'
countsTitle=document.createElement('div')
countsTitle.className='roundInfoTitle___VOdv2'
if(streakMode==='country') countsTitle.textContent='国家连击'
else countsTitle.textContent='一级行政区连击'
countsValue=document.createElement('div')
countsValue.className='roundInfoValue___zV6GS'
countsDiv.appendChild(countsTitle)
countsDiv.appendChild(countsValue)
const divider = document.createElement('div');
divider.classList.add('ant-divider', 'css-i874aq', 'ant-divider-vertical');
divider.setAttribute('role', 'separator');
panel_container.appendChild(divider)
panel_container.appendChild(countsDiv)
if(gameMode){
const divider_ = document.createElement('div');
divider_.classList.add('ant-divider', 'css-i874aq', 'ant-divider-vertical');
divider_.setAttribute('role', 'separator');
panel_container.appendChild(divider_)
const avgDiv=document.createElement('div')
avgDiv.className='roundInfoBox___ikizG'
const avgTitle=document.createElement('div')
avgTitle.className='roundInfoTitle___VOdv2'
avgTitle.textContent='平均分'
avgValue_=document.createElement('div')
avgValue_.className='roundInfoValue___zV6GS'
avgValue_.textContent=avgScore
avgDiv.appendChild(avgTitle)
avgDiv.appendChild(avgValue_)
panel_container.appendChild(avgDiv)
}
}
if(panel_container){
countsValue.textContent=streakCounts[mapsId][streakMode]
avgValue_.textContent=avgScore
}
}
function matchCountryCode(t) {
if(t.country==='印度'&&t.state==='阿鲁纳恰尔邦') t.country_code='CN'
else if(t.country==='India'&&t.state==='Arunachal Pradesh') t.country_code='CN'
if (t&&t.country_code){
const cc=t.country_code.toUpperCase()
if(CC_DICT[cc])return CC_DICT[cc]
else return cc
}
else return 'Undefined'
}
function matchState(t) {
if(!t) return 'Undefined'
if (t.state) {
return t.state;
}else if (t.province) {
return t.province;
} else if (t.territory) {
return t.territory;
} else if (t.state_district) {
return t.state_district;
} else if (t.county) {
return t.county;
} else {
return 'Undefined';
}
}
function formatNumber(number) {
const numberStr = number.toString();
const formattedNumber = numberStr.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return formattedNumber;
}
async function genShortLink(panoId){
const location=viewer.getPosition()
const POV=viewer.getPov()
const zoom=viewer.getZoom()
var shortUrl
if(panoId.length!=27) shortUrl=await getGoogleSL(panoId,location,POV.heading,POV.pitch,zoom);
else if (panoId.length==23) shortUrl=`https://map.qq.com/#from=web&heading=${POV.heading}&pano=${panoId}&pitch=${POV.pitch}&ref=web&zoom=${parseInt(zoom)}`
else shortUrl=await getBDSL(panoId,POV.heading,POV.pitch)
return shortUrl
}
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;
}
async function getGoogleSL(panoId, loc, h, t, z) {
const url = 'https://www.google.com/maps/rpc/shorturl';
const y=calculateFOV(z)
const pb = `!1shttps://www.google.com/maps/@${loc.lat()},${loc.lng()},3a,${y}y,${h}h,${t+90}t/data=*213m7*211e1*213m5*211s${panoId}*212e0*216shttps%3A%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fpanoid%3D${panoId}%26cb_client%3Dmaps_sv.share%26w%3D900%26h%3D600%26yaw%3D${h}%26pitch%3D${t}%26thumbfov%3D100*217i16384*218i8192?coh=205410&entry=tts&g_ep=EgoyMDI0MDgyOC4wKgBIAVAD!2m2!1sH5TSZpaqObbBvr0PvKOJ0AI!7e81!6b1`;
const params = new URLSearchParams({
authuser: '0',
hl: 'en',
gl: 'us',
pb: pb
}).toString();
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: `${url}?${params}`,
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
const text = response.responseText;
const match = text.match(/"([^"]+)"/);
if (match && match[1]) {
resolve(match[1]);
} else {
reject('No URL found.');
}
} catch (error) {
reject('Failed to parse response: ' + error);
}
} else {
reject('Request failed with status: ' + response.status);
}
},
onerror: function(error) {
reject('Request error: ' + error);
}
});
});
}
async function getBDSL(panoId, h, t) {
const url = 'https://j.map.baidu.com/?';
const target = `https://map.baidu.com/?newmap=1&shareurl=1&panoid=${panoId}&panotype=street&heading=${h}&pitch=${t}&l=13&tn=B_NORMAL_MAP&sc=0&newmap=1&shareurl=1&pid=${panoId}`;
const params = new URLSearchParams({
url: target,
web: 'true',
pcevaname: 'pc4.1',
newfrom:'zhuzhan_webmap',
callback:'jsonp94641768'
}).toString()
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: `${url}${params}`,
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
const data = response.responseText;
const urlRegex = /\((\{.*?\})\)$/;
const match = data.match(urlRegex);
if (match && match[1]) {
const jsonData = JSON.parse(match[1].replace(/\\\//g, '/'));
resolve(jsonData.url)
} else {
console.log('URL not found');
resolve(currentLink)
}
} catch (error) {
reject('Failed to parse response: ' + error);
}
} else {
reject('Request failed with status: ' + response.status);
}
},
onerror: function(error) {
reject('Request error: ' + error);
}
});
});
}
let onKeyDown =async (e) => {
if (e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) {
return;
}
if (e.key === 'p' || e.key === 'P') {
e.stopImmediatePropagation();
if(streakMode!='state')streakMode='state'
else streakMode='country'
countsTitle.textContent = streakMode === 'country' ? '国家连击' : '一级行政区连击';
countsValue.textContent=streakCounts[mapsId][streakMode]
localStorage.setItem('streakMode',JSON.stringify(streakMode))
Swal.fire({
title: '切换成功',
text:`${streakMode === 'country' ? '国家连击' : '一级行政区连击'}连击计数器已就绪`,
icon: 'success',
timer: 1200,
showConfirmButton: false,
});
}
else if ((e.shiftKey)&&(e.key === 'c' || e.key === 'C')){
const panoId=viewer.getPano()
const currentLink=await genShortLink(panoId)
if(currentLink){
GM_setClipboard(currentLink, 'text');
Swal.fire({
title: '复制成功',
text: '街景链接已复制到你的剪贴板中',
icon: 'success',
timer: 1000,
showConfirmButton: false,
});
}
}
}
document.addEventListener("keydown", onKeyDown);
})();