您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Tag your street views by date&address&generations
当前为
// ==UserScript== // @name Geoguessr Map-Making Auto-Tag // @namespace http://tampermonkey.net/ // @version 3.5 // @description Tag your street views by date&address&generations // @author KaKa // @match https://map-making.app/maps/* // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @require https://cdn.jsdelivr.net/npm/sweetalert2@11 // @license MIT // @icon https://www.google.com/s2/favicons?domain=geoguessr.com // ==/UserScript== (function() { 'use strict'; let tagBox = ['year', 'month', 'country', 'subdivision', 'locality', 'generations', 'type', 'coverageCount'] async function runScript(tags,sR) { const { value: option,dismiss: inputDismiss } = await Swal.fire({ title: 'Input JSON Data', text: 'Do you want to input data from the clipboard? If you click "Cancel", you will need to upload a JSON file.', icon: 'question', showCancelButton: true, showCloseButton:true, allowOutsideClick: false, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: 'Yes', cancelButtonText: 'Cancel' }); let data; if (option) { const text = await navigator.clipboard.readText(); try { data = JSON.parse(text); } catch (error) { Swal.fire('Error parsing JSON data! ', 'The input JSON data is invalid or incorrectly formatted.','error'); return; } } else if(inputDismiss==='cancel'){ const input = document.createElement('input'); input.type = 'file'; input.style.display = 'none' document.body.appendChild(input); data = await new Promise((resolve) => { input.addEventListener('change', async () => { const file = input.files[0]; const reader = new FileReader(); reader.onload = (event) => { try { const result = JSON.parse(event.target.result); resolve(result); document.body.removeChild(input); } catch (error) { Swal.fire('Error Parsing JSON Data!', 'The input JSON data is invalid or incorrectly formatted.','error'); } }; reader.readAsText(file); }); input.click(); }); } const newData = []; async function UE(t, e) { try { const r = `https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/${t}`; let payload=createPayload(t,e) const response = await fetch(r, { 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 { return await response.json(); } } catch (error) { console.error(`There was a problem with the UE function: ${error.message}`); } } function createPayload(mode,coorData) { let payload; if (mode === 'GetMetadata') { payload = [["apiv3",null,null,null,"US",null,null,null,null,null,[[0]]],["en","US"],[[[2,coorData.panoId]]],[[1,2,3,4,8,6]]]; } else if (mode === 'SingleImageSearch') { payload =[["apiv3",null,null,null,"US",null,null,null,null,null, [[0]]], [[null,null,coorData.lat,coorData.lng],50], [null,["en","US"],null,null,null,null,null,null,[2],null,[[[2,1,2],[3,1,2],[10,1,2]]]], [[1,2,3,4,8,6]]]; } else { throw new Error("Invalid mode!"); } return JSON.stringify(payload); } function getMetaData(svData) { if (svData) { let levelId=svData.dn let year = 'noyear',month = 'nomonth' let panoType='Unofficial' let subdivision='nosub',locality='nolocality' let coverageCount='0' if (svData.imageDate) { const matchYear = svData.imageDate.match(/\d{4}/); if (matchYear) { year = matchYear[0]; } const matchMonth = svData.imageDate.match(/-(\d{2})/); if (matchMonth) { month = matchMonth[1]; } } if (svData.copyright.includes('Google')) { panoType = 'Official'; } if (svData.time){ coverageCount = svData.time.length.toString(); } if(svData.location.description){ let parts = svData.location.description.split(','); if(parts.length > 1){ subdivision = parts[parts.length-1].trim(); locality = parts[parts.length-2].trim(); } else { subdivision = svData.location.description; } } return [year,month,panoType,subdivision,locality,levelId,coverageCount] } else{ return null} } function getGeneration(svData,country) { if (svData&&svData.tiles) { if (svData.tiles.worldSize.height === 1664) { // Gen 1 return 'Gen1'; } else if (svData.tiles.worldSize.height === 6656) { // Gen 2 or 3 let lat; for (let key in svData.Sv) { lat = svData.Sv[key].lat; break; } let date; if (svData.imageDate) { date = new Date(svData.imageDate); } else { date = 'nodata'; } if (date!=='nodata'&&((country === 'BD' && (date >= new Date('2021-04'))) || (country === 'EC' && (date >= new Date('2022-03'))) || (country === 'FI' && (date >= new Date('2020-09'))) || (country === 'IN' && (date >= new Date('2021-10'))) || (country === 'LK' && (date >= new Date('2021-02'))) || (country === 'KH' && (date >= new Date('2022-10'))) || (country === 'LB' && (date >= new Date('2021-05'))) || (country === 'NG' && (date >= new Date('2021-06'))) || (country === 'ST') || (country === 'US' && lat > 52 && (date >= new Date('2019-01'))))) { return 'Shitcam'; } let gen2Countries = ['AU', 'BR', 'CA', 'CL', 'JP', 'GB', 'IE', 'NZ', 'MX', 'RU', 'US', 'IT', 'DK', 'GR', 'RO', 'PL', 'CZ', 'CH', 'SE', 'FI', 'BE', 'LU', 'NL', 'ZA', 'SG', 'TW', 'HK', 'MO', 'MC', 'SM', 'AD', 'IM', 'JE', 'FR', 'DE', 'ES', 'PT']; if (gen2Countries.includes(country)) { return 'Gen2or3'; } else{ return 'Gen3';} } else if(svData.tiles.worldSize.height === 8192){ return 'Gen4'; } } return 'Unknown'; } var CHUNK_SIZE = 1200; var promises = []; async function getElevation(locations) { function findRange(elevation, ranges) { for (let i = 0; i < ranges.length; i++) { const range = ranges[i]; if (elevation >= range.min && elevation <= range.max) { return `${range.min}-${range.max}m`; } } if(!elevation){ return 'noElevation' } return `${JSON.stringify(elevation)}m`; } const batchSize = 100; const totalBatches = Math.ceil(locations.length / batchSize); for (let i = 0; i < totalBatches; i++) { const batchLocations = locations.slice(i * batchSize, (i + 1) * batchSize); const coordinates = batchLocations.map(location => `${location.lat},${location.lng}`).join('|'); const url = `https://api.open-elevation.com/api/v1/lookup?locations=${coordinates}`; try { const response = await fetch(url); const data = await response.json(); if (data && data.results && data.results.length > 0) { const elevations = data.results.map(result => result.elevation); batchLocations.forEach((location, index) => { if (location.extra && location.extra.tags) { if (sR) { const range = findRange(elevations[index], sR); location.extra.tags.push(`${range}`); } else { location.extra.tags.push(`${JSON.stringify(elevations[index])}`); } } else { location.extra = { tags: [(sR ? `${findRange(elevations[index], sR)}` : `${JSON.stringify(elevations[index])}`)] }; } }); } else { batchLocations.forEach(location => { if (location.extra && location.extra.tags) { location.extra.tags.push('noElevation'); } else{location.extra = { tags: ['noElevation'] } }; }); } } catch (error) { console.log(error); } } await Promise.all(promises); return locations; } async function processCoord(coord, tags, svData,ccData) { if (!coord.extra) { coord.extra = {}; } if (!coord.extra.tags) { coord.extra.tags = []; } if (svData){ let meta=getMetaData(svData) let yearTag=meta[0] let monthTag=meta[1] let typeTag=meta[2] let subdivisionTag=meta[3] let localityTag=meta[4] let countryTag let genTag let trekkerTag=meta[5] let coverageTag=meta[6] if (ccData){ try { countryTag = ccData[1][0][5][0][1][4] } catch (error) { try { countryTag = ccData[1][5][0][1][4] } catch (error) { countryTag='nocountry' } } if (!countryTag)countryTag='nocountry' } genTag = getGeneration(svData,countryTag) if (tags.includes('generation')&&typeTag=='Official')coord.extra.tags.push(genTag) if (tags.includes('year'))coord.extra.tags.push(yearTag) if (tags.includes('month'))coord.extra.tags.push(monthTag) if (tags.includes('type'))coord.extra.tags.push(typeTag) if (tags.includes('type')&&trekkerTag&&typeTag=='Official')coord.extra.tags.push('trekker') if (tags.includes('country')&&typeTag=='Official')coord.extra.tags.push(countryTag) if (tags.includes('subdivision')&&typeTag=='Official')coord.extra.tags.push(subdivisionTag) if (tags.includes('locality')&&typeTag=='Official')coord.extra.tags.push(localityTag) if (tags.includes('coverageCount')&&typeTag=='Official')coord.extra.tags.push(coverageTag) } else { if(tags.some(tag => tagBox.includes(tag))){ coord.extra.tags.push('nopano') } } if (coord.extra.tags) {coord.extra.tags=Array.from(new Set(coord.extra.tags))} newData.push(coord); } async function processChunk(chunk, tags) { if (tags.includes('elevation')){ try { chunk = await getElevation(chunk,); } catch (error) { console.error('error fecthing elevtion data:', error); } } var service = new google.maps.StreetViewService(); var promises = chunk.map(async coord => { let panoId = coord.panoId; if (!panoId) { if (coord.extra&&coord.extra.panoId){ panoId = coord.extra.panoId;} } let latLng = {lat: coord.lat, lng: coord.lng}; let svData; let ccData; if ((panoId || latLng)) { svData = await getSVData(service, panoId ? {pano: panoId} : {location: latLng, radius: 50}); } if (!panoId && (tags.includes('generation')||('country'))) { ccData = await UE('SingleImageSearch', coord); } else if (panoId && (tags.includes('generation')||('country'))) { ccData = await UE('GetMetadata', coord); } await processCoord(coord, tags, svData,ccData) }); await Promise.all(promises); } function getSVData(service, options) { return new Promise(resolve => service.getPanorama({...options}, (data, status) => { resolve(data); })); } async function processData(tags) { try { const totalChunks = Math.ceil(data.customCoordinates.length / CHUNK_SIZE); let processedChunks = 0; const swal = Swal.fire({ title: 'Processing Data', text: 'Please wait...', allowOutsideClick: false, allowEscapeKey: false, showConfirmButton: false, didOpen: () => { Swal.showLoading(); } }); for (let i = 0; i < data.customCoordinates.length; i += CHUNK_SIZE) { let chunk = data.customCoordinates.slice(i, i + CHUNK_SIZE); await processChunk(chunk, tags); processedChunks++; const progress = Math.min((processedChunks / totalChunks) * 100, 100); Swal.update({ html: `<div>${progress.toFixed(2)}% completed</div> <div class="swal2-progress"> <div class="swal2-progress-bar" role="progressbar" aria-valuenow="${progress}" aria-valuemin="0" aria-valuemax="100" style="width: ${progress}%;"> </div> </div>` }); } GM_setClipboard(JSON.stringify(newData)); swal.close(); Swal.fire({ title: 'Success!', text: 'New JSON data has been copied to the clipboard!', icon: 'success' }); } catch (error) { swal.close(); Swal.fire({ title: 'Error!', text: 'Invalid JSON data', icon: 'error' }); console.error('Error processing JSON data:', error); } } if(data.customCoordinates){ if(data.customCoordinates.length>=1){processData(tags);} else{Swal.fire('Error Parsing JSON Data!', 'The input JSON data is empty.','error');} }else{Swal.fire('Error Parsing JSON Data!', 'The input JSON data is invaild or incorrectly formatted.','error');} } function createCheckbox(text, tags) { var label = document.createElement('label'); var checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = text; checkbox.name = 'tags'; checkbox.id = tags; label.appendChild(checkbox); label.appendChild(document.createTextNode(text)); buttonContainer.appendChild(label); return checkbox; } var mainButton = document.createElement('button'); mainButton.textContent = 'Auto-Tag'; mainButton.style.position = 'fixed'; mainButton.style.right = '20px'; mainButton.style.bottom = '20px'; mainButton.style.borderRadius = "18px"; mainButton.style.fontSize ="16px"; mainButton.style.padding = "10px 20px"; mainButton.style.border = "none"; mainButton.style.color = "white"; mainButton.style.cursor = "pointer"; mainButton.style.backgroundColor = "#4CAF50"; mainButton.addEventListener('click', function() { if (buttonContainer.style.display === 'none') { buttonContainer.style.display = 'block'; } else { buttonContainer.style.display = 'none'; } }); document.body.appendChild(mainButton); var buttonContainer = document.createElement('div'); buttonContainer.style.position = 'fixed'; buttonContainer.style.right = '20px'; buttonContainer.style.bottom = '60px'; buttonContainer.style.display = 'none'; document.body.appendChild(buttonContainer); var triggerButton = document.createElement('button'); triggerButton.textContent = 'Star Tagging'; triggerButton.addEventListener('click', function() { var checkboxes = document.getElementsByName('tags'); var checkedTags = []; for (var i=0; i<checkboxes.length; i++) { if (checkboxes[i].checked) { checkedTags.push(checkboxes[i].id); } } if (checkedTags.includes('elevation')) { Swal.fire({ title: 'Set A Range For Elevation', text: 'Please set a range for the elevation. If you select "Cancel", the script will return the exact elevation for each location.', icon: 'question', showCancelButton: true, showCloseButton: true, allowOutsideClick: false, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: 'Yes', cancelButtonText: 'Cancel' }).then((result) => { if (result.isConfirmed){ Swal.fire({ title: 'Define Range for Each Segment', html: ` <label> <br>Enter range for each segment, separated by commas</br></label> <textarea id="segmentRanges" class="swal2-textarea" placeholder="such as:-1-10,11-35"></textarea> `, icon: 'question', showCancelButton: true, showCloseButton: true, allowOutsideClick: false, focusConfirm: false, preConfirm: () => { const segmentRangesInput = document.getElementById('segmentRanges').value.trim(); if (!segmentRangesInput) { Swal.showValidationMessage('Please enter range for each segment'); return false; } const segmentRanges = segmentRangesInput.split(','); const validatedRanges = segmentRanges.map(range => { const matches = range.trim().match(/^\s*(-?\d+)\s*-\s*(-?\d+)\s*$/); if (matches) { const min = Number(matches[1]); const max = Number(matches[2]); return { min, max }; } else { Swal.showValidationMessage('Invalid range format. Please use format: minValue-maxValue'); return false; } }); return validatedRanges.filter(Boolean); }, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: 'Yes', cancelButtonText: 'Cancel', inputValidator: (value) => { if (!value.trim()) { return 'Please enter range for each segment'; } } }).then((result) => { if (result.isConfirmed) { runScript(checkedTags,result.value) } else { Swal.showValidationMessage('You canceled input'); } });} }); } else{ runScript(checkedTags)} }) buttonContainer.appendChild(triggerButton); createCheckbox('Year', 'year'); createCheckbox('Month', 'month'); createCheckbox('Type', 'type'); createCheckbox('Country', 'country'); createCheckbox('Subdivision', 'subdivision'); createCheckbox('Locality', 'locality'); createCheckbox('Generations', 'generation'); createCheckbox('Coverage Count','coverageCount') createCheckbox('Elevation','elevation') })();