Geoguessr Map-Making Auto-Tag

Tag your street views by date&address&generation&elevation

当前为 2024-05-28 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Geoguessr Map-Making Auto-Tag
// @namespace    http://tampermonkey.net/
// @version      3.72
// @description  Tag your street views by date&address&generation&elevation
// @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.svgrepo.com/show/423677/tag-price-label.svg
// ==/UserScript==

(function() {
    'use strict';
    let accuracy=2 /* You could modifiy accuracy here, default setting is 1 minutes */

    let tagBox = ['Year', 'Month','Day', 'Time','Country', 'Subdivision', 'Locality', 'Generations', 'Type', 'CoverageCount','Elevation']
    async function runScript(tags,sR,uN) {
        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 = [];

        function mergeList(list1, list2) {
            const mergedDict = list1.reduce((dict, currentValue, index) => {

                if (list2[index] !== undefined) {
                    dict[currentValue] = list2[index];
                }
                return dict;
            }, {});

            return mergedDict;
        }

        async function UE(t, e,s,d) {
            try {
                const r = `https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/${t}`;
                let payload=createPayload(t,e)
                if(d){
                    payload=JSON.stringify([["apiv3"],[[null,null,e.lat,e.lng],10],[[null,null,null,null,null,null,null,null,null,null,[s,d]],null,null,null,null,null,null,null,[2],null,[[[2,true,2]]]],[[2,6]]])
                }
                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]]],[[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 monthToTimestamp(m) {

            const [year, month] = m.split('-');

            const startDate =Math.round( new Date(year, month-1,1).getTime()/1000);

            const endDate =Math.round( new Date(year, month, 1).getTime()/1000)-1;

            return { startDate, endDate };
        }

        async function binarySearch(c, start,end) {
            let capture
            let response
            while (end - start >= accuracy) {
                let mid= Math.round((start + end) / 2);
                response = await UE("SingleImageSearch", c, start,end);
                if (response&&response[0][2]== "Search returned no images." ){
                    start=mid+start-end
                    end=start-mid+end
                    mid=Math.round((start+end)/2)
                } else {
                    start=mid
                    mid=Math.round((start+end)/2)
                }
                capture=mid
            }

            return capture
        }

        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';
        }

        async function getLocal(coord, timestamp) {
            const apiUrl = "https://api.wheretheiss.at/v1/coordinates/";
            const systemTimezoneOffset = -new Date().getTimezoneOffset() * 60;

            try {
                const [lat, lng] = coord;
                const url = `${apiUrl}${lat},${lng}`;

                const response = await fetch(url);
                if (!response.ok) {
                    throw new Error("Request failed: " + response.statusText);
                }
                const data = await response.json();
                const targetTimezoneOffset = data.offset * 3600;
                const offsetDiff = systemTimezoneOffset - targetTimezoneOffset;
                const convertedTimestamp = Math.round(timestamp - offsetDiff);
                return convertedTimestamp;
            } catch (error) {
                throw error;
            }
        }

        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])}m`);
                                }
                            } else {
                                location.extra = {
                                    tags: [(sR ? `${findRange(elevations[index], sR)}m` : `${JSON.stringify(elevations[index])}m`)]
                                };
                            }
                        });
                    } 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;
        }

        var CHUNK_SIZE = 1200;
        var promises = [];

        async function processCoord(coord, tags, svData,ccData) {
            if (!coord.extra) {
                coord.extra = {};
            }
            if (!coord.extra.tags) {
                coord.extra.tags = [];
            }

            if (svData){
                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]
                let dayTag,timeTag,exactTime,timeRange
                var date=monthToTimestamp(meta[0]+'-'+meta[1])

                if(tags.includes('day')||tags.includes('time')){
                    exactTime=await binarySearch(coord, date.startDate,date.endDate)
                    if (exactTime<=date.startDate||exactTime>=date.endDate){
                    exactTime=null
                    }

                }

                if(!exactTime){dayTag='noday'
                              timeTag='notime'
                              }
                else{

                    const currentDate = new Date();
                    const currentOffset =-(currentDate.getTimezoneOffset())*60
                    const dayOffset = Math.round((coord.lng / 15) * 3600)-currentOffset;
                    const LocalDay=new Date(Math.round(exactTime-dayOffset)*1000)
                    dayTag = LocalDay.toISOString().split('T')[0];

                    if(tags.includes('time')) {

                        var localTime=await getLocal([coord.lat,coord.lng],exactTime)
                        var timeObject=new Date(localTime*1000)
                        timeTag =`${timeObject.getHours().toString().padStart(2, '0')}:${timeObject.getMinutes().toString().padStart(2, '0')}:${timeObject.getSeconds().toString().padStart(2, '0')}`;
                        var hour = timeObject.getHours();

                        if (hour < 11) {
                            timeRange = 'Morning';
                        } else if (hour >= 11 && hour < 13) {
                            timeRange = 'Noon';
                        } else if (hour >= 13 && hour < 17) {
                            timeRange = 'Afternoon';
                        } else if(hour >= 17 && hour < 19) {
                            timeRange = 'Evening';
                        }
                        else{
                            timeRange = 'Night';
                        }
                    }
                }
                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'
                }

                if (tags.includes('generation')&&typeTag=='Official'){
                    genTag = getGeneration(svData,countryTag)
                    coord.extra.tags.push(genTag)}

                    if (tags.includes('year'))coord.extra.tags.push(yearTag)

                    if (tags.includes('month'))coord.extra.tags.push(yearTag.slice(-2)+'-'+monthTag)

                    if (tags.includes('day'))coord.extra.tags.push(dayTag)

                    if (tags.includes('time')) coord.extra.tags.push(timeTag)

                    if (tags.includes('time')&&timeRange) coord.extra.tags.push(timeRange)

                    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)) {
                    if(tags!=['elevation']){
                    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', panoId);
                }

                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( 'Success!','New JSON data has been copied to the clipboard!','success');
            } catch (error) {
                swal.close();
                Swal.fire('Error!', 'Invalid JSON data','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';
    buttonContainer.style.fontSize='15px'
    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: '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 if (result.dismiss === Swal.DismissReason.cancel){runScript(checkedTags)}
            });
        }
        else{
            runScript(checkedTags)}
    })
    buttonContainer.appendChild(triggerButton);

    tagBox.forEach(tag => {
        createCheckbox(tag, tag.toLowerCase());
    });
})();