您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Tag your street view by date, exactTime, address, generation, elevation
当前为
// ==UserScript== // @name Geoguessr Map-Making Auto-Tag // @namespace https://greasyfork.org/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) })();