Geoguessr Map-Making Auto-Tag

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

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Geoguessr Map-Making Auto-Tag
// @namespace    https://greasyfork.org/users/1179204
// @version      3.86.4
// @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','Country', 'Subdivision', 'Generation', 'Elevation','Driving Direction','Type','Copyright','Update','Fix','Detect']
    let months = ['January', 'February', 'March', 'April', 'May', 'June','July', 'August', 'September', 'October', 'November', 'December'];
    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'
            if (svData) {
                console.log(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.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]
            }
            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: 'N', range: [337.5, 22.5] },
                { name: 'NE', range: [22.5, 67.5] },
                { name: 'E', range: [67.5, 112.5] },
                { name: 'SE', range: [112.5, 157.5] },
                { name: 'S', range: [157.5, 202.5] },
                { name: 'SW', range: [202.5, 247.5] },
                { name: 'W', range: [247.5, 292.5] },
                { name: 'NW', 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) {
            console.log(tags)

            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('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;">
            <div style="flex: 1; min-width: 150px;">
                ${firstHalf.map(tag => `
                    <label style="display: block; margin-bottom: 8px;">
                        <input type="checkbox" class="feature-checkbox" value="${tag}" /> ${tag}
                    </label>
                `).join('')}
            </div>
            <div style="flex: 1; min-width: 150px;">
                ${secondHalf.map(tag => `
                    <label style="display: block; margin-bottom: 8px;">
                        <input type="checkbox" class="feature-checkbox" value="${tag}" /> ${tag}
                    </label>
                `).join('')}
            </div>
            <div style="flex: 1; min-width: 150px; margin-top: 10px; text-align: center;">
                <label style="display: block;">
                    <input type="checkbox" class="feature-checkbox" id="selectAll" /> Select All
                </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)
})();