Geoguessr Map-Making Auto-Tag

Tag your street view by date, exactTime, address, generation, elevation

目前為 2024-07-28 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Geoguessr Map-Making Auto-Tag
// @namespace    https://gf.qytechs.cn/users/1179204
// @version      3.86.5
// @description  Tag your street view by date, exactTime, address, generation, elevation
// @author       KaKa
// @match        *://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 = 60 /* You could modifiy accuracy here, default setting is 60s */

    let tagBox = ['Year', 'Month', 'Day', 'Time', 'Type', 'Country', 'Subdivision', 'Generation', 'Elevation', 'Driving Direction', 'Reset Heading', 'Update', 'Fix', 'Detect']

    let months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

    let tooltips = {
        'Year': 'Year of street view capture in format yyyy',
        'Month': 'Month of street view capture in format yy-mm',
        'Day': 'Specific date of street view capture in format yyyy-mm-dd',
        'Time': 'Exact time of street view capture with optional time range description, e.g., 09:35:21 marked as Morning',
        'Country': 'Country of street view location (Google data)',
        'Subdivision': 'Primary administrative subdivision of street view location',
        'Generation': 'Camera generation of the street view, categorized as Gen1, Gen2orGen3, Gen3, Gen4, Shitcam',
        'Elevation': 'Elevation of street view location (Google data)',
        'Type': 'Type of street view, categorized as Official, Unofficial, Trekker (may include floor ID if available)',
        'Driving Direction': 'Absolute driving direction of street view vehicle',
        'Reset Heading': 'Reset heading to default driving direction of street view vehicle',
        'Fix': 'Fix broken locs by updating to latest coverage or searching for specific coverage based on saved date from map-making',
        'Update': 'Update street view to latest coverage or based on saved date from map-making, effective only for locs with panoID',
        'Detect': 'Detect street views that are about to be removed and mark it as "Dangerous" '
    };
    let mapData

    function getMap() {
        return new Promise(function (resolve, reject) {
            var requestURL = window.location.origin + "/api" + window.location.pathname + "/locations";
            fetch(requestURL, {
                headers: {
                    'Accept': 'application/json',
                    'Content-Encoding': 'gzip'
                }
            })
                .then(function (response) {
                    if (!response.ok) {
                        throw new Error('HTTP error, status = ' + response.status);
                    }
                    return response.json();
                })
                .then(function (jsonData) {
                    resolve(jsonData);
                })
                .catch(function (error) {
                    console.error('Fetch Error:', error);
                    reject('Error fetching meta data of the map!');
                });
        });
    }

    async function getSelection() {
        return new Promise((resolve, reject) => {
            var exportButtonText = 'Export';
            var buttons = document.querySelectorAll('button.button');

            for (var i = 0; i < buttons.length; i++) {
                if (buttons[i].textContent.trim() === exportButtonText) {
                    buttons[i].click();
                    var modalDialog = document.querySelector('.modal__dialog.export-modal');
                }
            }

            setTimeout(() => {
                const radioButton = document.querySelector('input[type="radio"][name="selection"][value="1"]');
                const spanText = radioButton.nextElementSibling.textContent.trim();
                if (spanText === "Export selection (0 locations)") {
                    swal.fire('Selection not found!', 'Please select at least one location as selection!', 'warning')
                    reject(new Error('Export selection is empty!'));
                }
                if (radioButton) radioButton.click()
                else {
                    reject(new Error('Radio button not found'));
                }
            }, 100);


            setTimeout(() => {
                const copyButton = document.querySelector('.export-modal__export-buttons button:first-of-type');
                if (!copyButton) {
                    reject(new Error('Copy button not found'));
                }
                copyButton.click();

            }, 200);
            setTimeout(() => {
                const closeButton = document.querySelector('.modal__close');
                if (closeButton) closeButton.click();
                else reject(new Error('Close button not found'));
            }, 400);

            setTimeout(async () => {
                try {
                    const data = await navigator.clipboard.readText()
                    const selection = JSON.parse(data);
                    resolve(selection);
                } catch (error) {
                    console.error("Error getting selection:", error);
                    reject(error);
                }
            }, 800);
        });
    }


    function matchSelection(selection, locations) {
        const matchingLocations = [];
        const customCoordinates = selection.customCoordinates;

        const locationMap = {};
        locations.forEach(loc => {
            const locString = JSON.stringify(loc.location);
            locationMap[locString] = loc;
        });

        for (const coord of customCoordinates) {
            const coordString = JSON.stringify({ lat: coord.lat, lng: coord.lng });
            if (locationMap.hasOwnProperty(coordString)) {
                const matchingLoc = locationMap[coordString];
                if (coord.extra.hasOwnProperty('panoDate') && coord.extra.panoDate) {
                    matchingLoc.panoDate = coord.panoDate;
                }
                matchingLocations.push(matchingLoc);
            }
        }

        return matchingLocations;
    }

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

    function updateSelection(entries) {
        var requestURL = window.location.origin + "/api" + window.location.pathname + "/locations";
        var payload = {
            edits: []
        };

        entries.forEach(function (entry) {
            var createEntry = {
                id: -1,
                author: entry.author,
                mapId: entry.mapId,
                location: entry.location,
                panoId: entry.panoId,
                panoDate: entry.panoDate,
                heading: entry.heading,
                pitch: entry.pitch,
                zoom: entry.zoom,
                tags: entry.tags,
                flags: entry.flags,
                createdAt: entry.createdAt,

            };
            payload.edits.push({
                action: {
                    type: 3
                },
                create: [createEntry],
                remove: [entry.id]
            });
        });

        var xhr = new XMLHttpRequest();
        xhr.open("POST", requestURL);
        xhr.setRequestHeader("Content-Type", "application/json");

        xhr.onload = function () {
            if (xhr.status >= 200 && xhr.status < 300) {
                console.log("Request succeeded");
            } else {
                console.error("Request failed with status", xhr.status);
            }
        };

        xhr.onerror = function () {
            swal.fire({
                icon: 'error',
                title: 'Oops...',
                text: 'Failed to update the map! Please retrieve JSON data from your clipboard.'
            });
        };

        xhr.send(JSON.stringify(payload));
    }

    async function runScript(tags, sR) {
        let taggedLocs = [];
        let exportMode, selections, fixStrategy

        if (tags.length < 1) {
            swal.fire('Feature not found!', 'Please select at least one feature!', 'warning')
            return
        }
        if (tags.includes('fix')) {
            const { value: fixOption, dismiss: fixDismiss } = await Swal.fire({
                title: 'Fix Strategy',
                icon: 'question',
                text: 'Would you like to fix the location based on the map-making data. (more suitable for those locs with a specific date coverage) Else it will update the broken loc with recent coverage.',
                showCancelButton: true,
                showCloseButton: true,
                allowOutsideClick: false,
                confirmButtonColor: '#3085d6',
                cancelButtonColor: '#d33',
                confirmButtonText: 'Yes',
                cancelButtonText: 'No',

            })
            if (fixOption) fixStrategy = 'exactly'
            else if (!fixOption && fixDismiss === 'cancel') {
                fixStrategy = null
            }
            else {
                return
            }
        };

        const { value: option, dismiss: inputDismiss } = await Swal.fire({
            title: 'Export',
            text: 'Do you want to update and save your map? If you click "Cancel", the script will just paste JSON data to the clipboard after finish tagging.',
            icon: 'question',
            showCancelButton: true,
            showCloseButton: true,
            allowOutsideClick: false,
            confirmButtonColor: '#3085d6',
            cancelButtonColor: '#d33',
            confirmButtonText: 'Yes',
            cancelButtonText: 'Cancel'
        });

        if (option) {
            exportMode = 'save'
        }
        else if (!selections && inputDismiss === 'cancel') {
            exportMode = null
        }
        else {
            return
        }

        const loadingSwal = Swal.fire({
            title: 'Preparing',
            text: 'Fetching selected locs from map-making. Please wait...',
            allowOutsideClick: false,
            allowEscapeKey: false,
            showConfirmButton: false,
            icon: "info",
            didOpen: () => {
                Swal.showLoading();
            }
        });
        const selectedLocs = await getSelection()
        mapData = await getMap()
        selections = await matchSelection(selectedLocs, mapData)
        loadingSwal.close()
        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, s, d);

                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, s, d, r) {
            let payload;
            if (!r) r = 50 // default search radius
            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') {
                var lat = coorData.lat;
                var lng = coorData.lng;
                lat = lat % 1 !== 0 && lat.toString().split('.')[1].length > 6 ? parseFloat(lat.toFixed(6)) : lat;
                lng = lng % 1 !== 0 && lng.toString().split('.')[1].length > 6 ? parseFloat(lng.toFixed(6)) : lng;
                if (s && d) {
                    payload = [["apiv3"], [[null, null, lat, lng], r], [[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]]]
                } else {
                    payload = [["apiv3"],
                    [[null, null, lat, lng], r],
                    [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, 10);
                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) {
            let year = 'Year not found', month = 'Month not found'
            let panoType = 'unofficial'
            let subdivision = 'Subdivision not found'
            let defaultHeading = null
            if (svData) {
                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.tiles && svData.tiles && svData.tiles.originHeading) {
                    defaultHeading = svData.tiles.originHeading
                }
                if (svData.location.description) {
                    let parts = svData.location.description.split(',');
                    if (parts.length > 1) {
                        subdivision = parts[parts.length - 1].trim();
                    } else {
                        subdivision = svData.location.description;
                    }
                }
                return [year, month, panoType, subdivision, defaultHeading]
            }
            else {
                return null
            }
        }

        function extractDate(array) {
            var year, month
            array.forEach(element => {
                const yearRegex1 = /^(\d{2})-(\d{2})$/;
                const yearRegex2 = /^(\d{4})-(\d{2})$/;
                const yearRegex3 = /^(\d{4})$/;
                const monthRegex1 = /^(\d{2})$/;
                const monthRegex2 = /^(January|February|March|April|May|June|July|August|September|October|November|December)$/i;

                if (!year && !month && yearRegex1.test(element)) {
                    const match = yearRegex1.exec(element);
                    year = parseInt(match[1]) + 2000;
                    month = parseInt(match[2]);
                }

                if (!year && !month && yearRegex2.test(element)) {
                    const match = yearRegex2.exec(element);
                    year = parseInt(match[1]);
                    month = parseInt(match[2]);
                }
                if (!year && yearRegex3.test(element)) {
                    const match = yearRegex3.test(element);
                    year = parseInt(element)
                }
                if (!month && monthRegex1.test(element)) {
                    month = parseInt(element);
                }

                if (!month && monthRegex2.test(element)) {
                    const months = {
                        "January": 1, "February": 2, "March": 3, "April": 4,
                        "May": 5, "June": 6, "July": 7, "August": 8,
                        "September": 9, "October": 10, "November": 11, "December": 12
                    };
                    month = months[element];
                }
            });
            return { year, month }
        }

        function getDirection(heading) {
            if (typeof heading !== 'number' || heading < 0 || heading >= 360) {
                return 'Unknown direction';
            }
            const directions = [
                { name: 'North', range: [337.5, 22.5] },
                { name: 'Northeast', range: [22.5, 67.5] },
                { name: 'East', range: [67.5, 112.5] },
                { name: 'Southeast', range: [112.5, 157.5] },
                { name: 'South', range: [157.5, 202.5] },
                { name: 'Southwest', range: [202.5, 247.5] },
                { name: 'West', range: [247.5, 292.5] },
                { name: 'Northwest', range: [292.5, 337.5] }
            ];

            for (const direction of directions) {
                const [start, end] = direction.range;
                if (start <= end) {
                    if (heading >= start && heading < end) {
                        return direction.name;
                    }
                } else {
                    if (heading >= start || heading < end) {
                        return direction.name;
                    }
                }
            }

            return 'Unknown direction';
        }

        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 systemTimezoneOffset = -new Date().getTimezoneOffset() * 60;
            try {
                const [lat, lng] = coord;
                const url = `https://api.wheretheiss.at/v1/coordinates/${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;
            }
        }

        var CHUNK_SIZE = 1200;
        if (tags.includes('time')) {
            CHUNK_SIZE = 500
        }
        var promises = [];

        async function processCoord(coord, tags, svData, ccData) {
            try {
                if (svData || ccData) {

                    var panoYear, panoMonth
                    if (coord.panoDate) {
                        panoYear = parseInt(coord.panoDate.substring(0, 4))
                        panoMonth = parseInt(coord.panoDate.substring(5, 7))
                    }
                    else if (coord.panoId) {
                        panoYear = parseInt(svData.imageDate.substring(0, 4))
                        panoMonth = parseInt(svData.imageDate.substring(5, 7))
                    }
                    else {
                        panoYear = parseInt(extractDate(coord.tags).year)
                        panoMonth = parseInt(extractDate(coord.tags).month)
                    }

                    let meta = getMetaData(svData)
                    let yearTag = meta[0]
                    let monthTag = parseInt(meta[1])
                    let typeTag = meta[2]
                    let subdivisionTag = meta[3]
                    let countryTag, elevationTag
                    let genTag, trekkerTag, floorTag, driDirTag
                    let dayTag, timeTag, exactTime, timeRange

                    //if(monthTag){monthTag=months[monthTag-1]}
                    monthTag = yearTag.slice(-2) + '-' + (monthTag.toString())
                    if (!monthTag) { monthTag = 'Month not found' }

                    var date = monthToTimestamp(svData.imageDate)

                    if (tags.includes('day') || tags.includes('time')) {
                        const initialSearch = await UE('SingleImageSearch', { 'lat': coord.location.lat, 'lng': coord.location.lng }, date.startDate, date.endDate)
                        if (initialSearch) {
                            if (initialSearch.length != 3) exactTime = null;
                            else {
                                exactTime = await binarySearch({ 'lat': coord.location.lat, 'lng': coord.location.lng }, date.startDate, date.endDate)
                            }
                        }

                    }

                    if (!exactTime) {
                        dayTag = 'Day not found'
                        timeTag = 'Time not found'
                    }
                    else {

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

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

                            var localTime = await getLocal([coord.location.lat, coord.location.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';
                            }
                        }
                    }

                    try {
                        if (ccData.length != 3) ccData = ccData[1][0]
                        else ccData = ccData[1]
                    }

                    catch (error) {
                        ccData = null
                    }

                    if (ccData) {
                        try {
                            countryTag = ccData[5][0][1][4]
                        }
                        catch (error) {
                            countryTag = null
                        }
                        try {
                            elevationTag = ccData[5][0][1][1][0]
                        }
                        catch (error) {
                            elevationTag = null
                        }
                        try {
                            driDirTag = ccData[5][0][1][2][0]
                        }
                        catch (error) {
                            driDirTag = null
                        }
                        try {
                            trekkerTag = ccData[6][5]
                        }
                        catch (error) {
                            trekkerTag = null
                        }
                        try {
                            floorTag = ccData[5][0][1][3][2][0]
                        }
                        catch (error) {
                            floorTag = null
                        }
                        if (tags.includes('detect')) {
                            const defaultDate = 3
                        }
                    }

                    if (trekkerTag) {
                        trekkerTag = trekkerTag.toString()
                        if (trekkerTag.includes('scout')) {
                            trekkerTag = 'trekker'
                        }
                        else {
                            trekkerTag = null
                        }
                    }

                    if (elevationTag) {
                        elevationTag = Math.round(elevationTag * 100) / 100
                        if (sR) {
                            elevationTag = findRange(elevationTag, sR)
                        }
                        else {
                            elevationTag = elevationTag.toString() + 'm'
                        }
                    }
                    if (driDirTag) {
                        driDirTag = getDirection(parseFloat(driDirTag))
                    }
                    else {
                        driDirTag = 'Driving direction not found'
                    }
                    if (!countryTag) countryTag = 'Country not found'
                    if (!elevationTag) elevationTag = 'Elevation not found'

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

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

                    if (tags.includes('month')) coord.tags.push(monthTag)

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

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

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

                    if (tags.includes('type')) coord.tags.push(typeTag)

                    if (tags.includes('driving direction')) coord.tags.push(driDirTag)

                    if (tags.includes('type') && trekkerTag && typeTag == 'Official') coord.tags.push('trekker')

                    if (tags.includes('type') && floorTag && typeTag == 'Official') coord.tags.push(floorTag)

                    if (tags.includes('country')) coord.tags.push(countryTag)

                    if (tags.includes('subdivision') && typeTag == 'Official') coord.tags.push(subdivisionTag)

                    if (tags.includes('elevation')) coord.tags.push(elevationTag)

                    if (tags.includes('reset heading')) {
                        if (meta[4]) coord.heading = meta[4]
                    }

                    if (tags.includes('update')) {
                        try {
                            const resultPano = await UE('SingleImageSearch', { lat: coord.location.lat, lng: coord.location.lng }, null, null, 10)
                            const updatedPnaoId = resultPano[1][1][1]
                            const updatedYear = resultPano[1][6][7][0]
                            const updatedMonth = resultPano[1][6][7][1]
                            if (coord.panoId) {
                                if (updatedPnaoId && updatedPnaoId != coord.panoId) {
                                    if (panoYear != updatedYear || panoMonth != updatedMonth) {
                                        coord.panoId = updatedPnaoId
                                        coord.tags.push('Updated')
                                    }
                                    else {
                                        coord.panoId = updatedPnaoId
                                        coord.tags.push('Copyright changed')
                                    }
                                }
                            }
                            else {
                                if (panoYear && panoMonth && updatedYear && updatedMonth) {
                                    if (panoYear != updatedYear || panoMonth != updatedMonth) {
                                        coord.panoId = updatedPnaoId
                                        coord.tags.push('Updated')
                                    }
                                }
                                else {
                                    coord.tags.push('Failed to update')
                                }
                            }
                        }
                        catch (error) {
                            coord.tags.push('Failed to update')
                        }
                    }
                }
            }
            catch (error) {
                if (!tags.includes('fix')) coord.tags.push('Pano not found');
                else {
                    var fixState
                    try {
                        const resultPano = await UE('SingleImageSearch', { lat: coord.location.lat, lng: coord.location.lng }, null, null, 5)
                        if (fixStrategy) {
                            const panos = resultPano[1][5][0][8]
                            for (const pano of panos) {
                                if ((pano[1][0] === panoYear && pano[1][1] === panoMonth)) {
                                    const panoIndex = pano[0]
                                    const fixedPanoId = resultPano[1][5][0][3][0][panoIndex][0][1]
                                    coord.panoId = fixedPanoId
                                    coord.location.lat = resultPano[1][5][0][1][0][2]
                                    coord.location.lng = resultPano[1][5][0][1][0][3]
                                    fixState = true
                                }
                            }
                        }
                        else {
                            coord.panoId = resultPano[1][1][1]
                            fixState = true
                        }

                    }
                    catch (error) {
                        fixState = null
                    }
                    if (!fixState) coord.tags.push('Failed to fix')
                    else coord.tags.push('Fixed')

                }
            }

            if (coord.tags) { coord.tags = Array.from(new Set(coord.tags)) }
            taggedLocs.push(coord);
        }

        async function processChunk(chunk, tags) {
            var service = new google.maps.StreetViewService();
            var promises = chunk.map(async coord => {
                let panoId = coord.panoId;
                let latLng = { lat: coord.location.lat, lng: coord.location.lng };
                let svData;
                let ccData;
                if ((panoId || latLng)) {
                    if (tags != ['country'] && tags != ['elevation'] && tags != ['detect']) {
                        svData = await getSVData(service, panoId ? { pano: panoId } : { location: latLng, radius: 50 });
                    }
                }

                if (tags.includes('generation') || ('country') || ('elevation') || ('type') || ('driving direction')) {
                    if (!panoId) ccData = await UE('SingleImageSearch', latLng);
                    else ccData = await UE('GetMetadata', panoId);
                }


                if (latLng && (tags.includes('detect'))) {
                    var detectYear, detectMonth
                    if (coord.panoDate) {
                        detectYear = parseInt(coord.panoDate.substring(0, 4))
                        detectMonth = parseInt(coord.panoDate.substring(5, 7))
                    }
                    else {
                        if (coord.panoId) {
                            const metaData = await getSVData(service, { pano: panoId })
                            if (metaData) {
                                if (metaData.imageDate) {
                                    detectYear = parseInt(metaData.imageDate.substring(0, 4))
                                    detectMonth = parseInt(metaData.imageDate.substring(5, 7))
                                }
                            }
                        }
                    }
                    if (detectYear && detectMonth) {
                        const metaData = await UE('SingleImageSearch', latLng, 10);
                        if (metaData) {
                            if (metaData.length > 1) {
                                const defaultDate = metaData[1][6][7]
                                if (defaultDate[0] === detectYear && defaultDate[1] != detectMonth) {
                                    coord.tags.push('Dangerous')
                                }
                            }
                        }
                    }
                }
                if (tags != ['detect']) {
                    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) {
            let successText = 'The JSON data has been pasted to your clipboard!';
            try {
                const totalChunks = Math.ceil(selections.length / CHUNK_SIZE);
                let processedChunks = 0;

                const swal = Swal.fire({
                    title: 'Tagging',
                    text: 'If you try to tag a large number of locs by exact time or elevation, it could take quite some time. Please wait...',
                    allowOutsideClick: false,
                    allowEscapeKey: false,
                    showConfirmButton: false,
                    icon: "info",
                    didOpen: () => {
                        Swal.showLoading();
                    }
                });

                for (let i = 0; i < selections.length; i += CHUNK_SIZE) {
                    let chunk = selections.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>`
                    });
                    if (exportMode) {
                        updateSelection(chunk)
                        successText = 'Tagging completed! Do you want to refresh the page?(The JSON data is also pasted to your clipboard)'
                    }
                }

                swal.close();
                var newJSON = []
                taggedLocs.forEach((loc) => {
                    newJSON.push({
                        lat: loc.location.lat,
                        lng: loc.location.lng,
                        heading: loc.heading,
                        pitch: loc.pitch !== undefined && loc.pitch !== null ? loc.pitch : 90,
                        zoom: loc.zoom !== undefined && loc.zoom !== null ? loc.zoom : 0,
                        panoId: loc.panoId,
                        extra: { tags: loc.tags }
                    })
                })
                GM_setClipboard(JSON.stringify(newJSON))
                Swal.fire({
                    title: 'Success!',
                    text: successText,
                    icon: 'success',
                    showCancelButton: true,
                    confirmButtonColor: '#3085d6',
                    cancelButtonColor: '#d33',
                    confirmButtonText: 'OK'
                }).then((result) => {
                    if (result.isConfirmed) {
                        if (exportMode) {
                            location.reload();
                        }
                    }
                });
            } catch (error) {
                swal.close();
                Swal.fire('Error Tagging!', '', 'error');
                console.error('Error processing JSON data:', error);
            }
        }

        if (selections) {
            if (selections.length >= 1) { processData(tags); }
            else {
                Swal.fire('Error Parsing JSON Data!', 'The input JSON data is empty! If you update the map after the page is loaded, please save it and refresh the page before tagging', 'error');
            }
        } else { Swal.fire('Error Parsing JSON Data!', 'The input JSON data is invaild or incorrectly formatted.', 'error'); }
    }

    function generateCheckboxHTML(tags) {

        const half = Math.ceil(tags.length / 2);
        const firstHalf = tags.slice(0, half);
        const secondHalf = tags.slice(half);

        return `
        <div style="display: flex; flex-wrap: wrap; gap: 10px; text-align: left;">
            <div style="flex: 1; min-width: 150px;">
                ${firstHalf.map((tag, index) => `
                    <label style="display: block; margin-bottom: 12px; margin-left: 40px; font-size: 15px;" title="${tooltips[tag]}">
                        <input type="checkbox" class="feature-checkbox" value="${tag}" /> <span style="font-size: 14px;">${tag}</span>
                    </label>
                `).join('')}
            </div>
            <div style="flex: 1; min-width: 150px;">
                ${secondHalf.map((tag, index) => `
                    <label style="display: block; margin-bottom: 12px; margin-left: 40px; font-size: 15px;" title="${tooltips[tag]}">
                        <input type="checkbox" class="feature-checkbox" value="${tag}" /> <span style="font-size: 14px;">${tag}</span>
                    </label>
                `).join('')}
            </div>
            <div style="flex: 1; min-width: 150px; margin-top: 12px; text-align: center;">
                <label style="display: block; font-size: 14px;">
                    <input type="checkbox" class="feature-checkbox" id="selectAll" /> <span style="font-size: 16px;">Select All</span>
                </label>
            </div>
        </div>
    `;
    }

    function showFeatureSelectionPopup() {
        const checkboxesHTML = generateCheckboxHTML(tagBox);

        Swal.fire({
            title: 'Select Features',
            html: `
            ${checkboxesHTML}
        `,
            icon: 'question',
            showCancelButton: true,
            showCloseButton: true,
            allowOutsideClick: false,
            confirmButtonColor: '#3085d6',
            cancelButtonColor: '#d33',
            confirmButtonText: 'Start Tagging',
            cancelButtonText: 'Cancel',
            didOpen: () => {
                const selectAllCheckbox = Swal.getPopup().querySelector('#selectAll');
                const featureCheckboxes = Swal.getPopup().querySelectorAll('.feature-checkbox:not(#selectAll)');

                selectAllCheckbox.addEventListener('change', () => {
                    featureCheckboxes.forEach(checkbox => {
                        checkbox.checked = selectAllCheckbox.checked;
                    });
                });


                featureCheckboxes.forEach(checkbox => {
                    checkbox.addEventListener('change', () => {

                        const allChecked = Array.from(featureCheckboxes).every(checkbox => checkbox.checked);
                        selectAllCheckbox.checked = allChecked;
                    });
                });
            },
            preConfirm: () => {
                const selectedFeatures = [];
                const featureCheckboxes = Swal.getPopup().querySelectorAll('.feature-checkbox:not(#selectAll)');

                featureCheckboxes.forEach(checkbox => {
                    if (checkbox.checked) {
                        selectedFeatures.push(checkbox.value.toLowerCase());
                    }
                });

                return selectedFeatures;
            }
        }).then((result) => {
            if (result.isConfirmed) {
                const selectedFeatures = result.value;
                handleSelectedFeatures(selectedFeatures);
            } else if (result.dismiss === Swal.DismissReason.cancel) {
                console.log('User canceled');
            }
        });
    }


    function handleSelectedFeatures(features) {
        if (features.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(features, result.value);
                        } else {
                            Swal.showValidationMessage('You canceled input');
                        }
                    });
                } else if (result.dismiss === Swal.DismissReason.cancel) {
                    runScript(features);
                }
            });
        } else {
            runScript(features);
        }
    }
    var mainButton = document.createElement('button');
    mainButton.textContent = 'Auto-Tag';
    mainButton.id = 'main-button';
    mainButton.style.position = 'fixed';
    mainButton.style.right = '20px';
    mainButton.style.bottom = '15px';
    mainButton.style.borderRadius = '18px';
    mainButton.style.fontSize = '15px';
    mainButton.style.padding = '10px 20px';
    mainButton.style.border = 'none';
    mainButton.style.color = 'white';
    mainButton.style.cursor = 'pointer';
    mainButton.style.backgroundColor = '#4CAF50';
    mainButton.addEventListener('click', showFeatureSelectionPopup);
    document.body.appendChild(mainButton)
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址