// ==UserScript== // @name Pano Detective // @namespace https://greasyfork.org/users/1179204 // @version 2.2.7 // @description Find the exact time a Google Street View image was taken (default coverage) // @author KaKa // @match *://maps.google.com/* // @match *://*.google.com/maps/* // @match *://*.google.ru/maps/* // @match *://*.google.de/maps/* // @match *://*.google.fr/maps/* // @match *://*.google.ca/maps/* // @match *://*.google.it/maps/* // @match *://*.google.at/maps/* // @match *://*.google.pl/maps/* // @match *://*.google.se/maps/* // @match *://*.google.ro/maps/* // @match *://*.google.cz/maps/* // @match *://*.google.sk/maps/* // @match *://*.google.nl/maps/* // @match *://*.google.si/maps/* // @match *://*.google.co.uk/maps/* // @match *://*.google.co.jp/maps/* // @match *://*.google.co.id/maps/* // @match *://*.google.co.in/maps/* // @match *://*.google.co.kr/maps/* // @match *://*.google.co.za/maps/* // @match *://*.google.co.th/maps/* // @match *://*.google.com.hk/maps/* // @match *://*.google.com.br/maps/* // @match *://*.google.com.mx/maps/* // @match *://*.google.com.ph/maps/* // @match *://*.google.com.ar/maps/* // @match *://*.google.com.co/maps/* // @match *://*.google.com.tw/maps/* // @exclude https://ogs.google.com // @exclude https://accounts.google.com // @exclude https://clients5.google.com // @icon https://www.svgrepo.com/show/485785/magnifier.svg // @require https://cdn.jsdelivr.net/npm/sweetalert2@11 // @require https://cdn.jsdelivr.net/npm/chinese-lunar@0.1.4/lib/chinese-lunar.min.js // @require https://cdn.jsdelivr.net/npm/browser-geo-tz@0.1.0/dist/geotz.min.js // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @connect vs-update-map.netlify.app // @license BSD // @downloadURL https://update.greasyfork.icu/scripts/497741/Pano%20Detective.user.js // @updateURL https://update.greasyfork.icu/scripts/497741/Pano%20Detective.meta.js // ==/UserScript== (function() { const DEFAULT={ DATE_FORMAT: 0, TIME_FORMAT: 1, ACCURACY: 2, LENGTH_UNITS: 'METERS', MAP_MAKING_API_KEY: 'PASTE_YOUR_KEY_HERE', NUMBER_OF_RECENT_MAPS: 3, NEARBY_CHECK:0, FULL_CHECK:true, SPEED_SHOW:true }; let CONFIG=JSON.parse(localStorage.getItem("PANO_DETECTIVE_CONFIG")); if(!CONFIG) CONFIG = DEFAULT GM_addStyle(` .mwstmm-modal { position: fixed; inset: 0; z-index: 99999; display: flex; align-items: center; justify-content: center; flex-direction: column; } .mwstmm-modal .dim { position: fixed; inset: 0; z-index: 0; background: rgba(0,0,0,0.75); } .mwstmm-modal .text { position: relative; z-index: 1; } .mwstmm-modal .inner { box-sizing: border-box; position: relative; z-index: 1; background: #fff; padding: 20px; margin: 20px; width: calc(100% - 40px); max-width: 500px; overflow: auto; color: #000; flex: 0 1 auto; } #mwstmm-loader { color: #fff; font-weight: bold; } .mwstmm-settings { position: absolute; top: 1rem; left: 1rem; z-index: 9; display: flex; flex-direction: column; gap: 5px; align-items: flex-start; } #note-btn { position: absolute; width:40px; height:40px; top: 0.85rem; right: 7.2rem; z-index: 9; display: flex; border: none; border-radius: 50%; background: #00000099; background-repeat: no-repeat; background-position:50%; flex-direction: column; gap: 5px; align-items: flex-start; } #note-btn:hover{ cursor: pointer; opacity:0.8; } #note-btn::after{ display:none; content: attr(data-text); position:absolute; top:120%; left:50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 1); color: #fff; padding: 5px; border-radius: 5px; font-weight:normal; font-size: 11px; line-height: 1; height: auto; white-space: nowrap; transition: opacity 0.5s ease; } #note-btn:hover::after { opacity: 1; display: block; } #settings-btn { position: absolute; width:40px; height:40px; top: 0.85rem; right: 7.2rem; z-index: 9; display: flex; border: none; border-radius: 50%; background: #00000099; background-repeat: no-repeat; background-position:50%; flex-direction: column; gap: 5px; align-items: flex-start; } #settings-btn:hover{ cursor: pointer; opacity:0.8; } #settings-btn::after{ display:none; content: attr(data-text); position:absolute; top:120%; left:50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 1); color: #fff; padding: 5px; border-radius: 5px; font-weight:normal; font-size: 11px; line-height: 1; height: auto; white-space: nowrap; transition: opacity 0.5s ease; } #settings-btn:hover::after { opacity: 1; display: block; } #mwstmm-main { position: absolute; width:40px; height:40px; top: 0.85rem; right: 4rem; z-index: 9; display: flex; border: none; border-radius: 50%; background: #00000099; background-repeat: no-repeat; background-position:50%; flex-direction: column; gap: 5px; align-items: flex-start; } #mwstmm-main:hover{ cursor: pointer; opacity:0.8; } #mwstmm-main::after{ display:none; content: attr(data-text); position:absolute; top:120%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 1); color: #fff; padding: 5px; border-radius: 5px; font-weight:normal; font-size: 11px; line-height: 1; height: auto; white-space: nowrap; transition: opacity 0.5s ease; } #mwstmm-main:hover::after { opacity: 1; display: block; } .mwstmm-settings.extra-pad { top: 2.5rem; } .mwstmm-title { font-size: 15px; font-weight: bold; text-shadow: rgb(204, 48, 46) 2px 0px 0px, rgb(204, 48, 46) 1.75517px 0.958851px 0px, rgb(204, 48, 46) 1.0806px 1.68294px 0px, rgb(204, 48, 46) 0.141474px 1.99499px 0px, rgb(204, 48, 46) -0.832294px 1.81859px 0px, rgb(204, 48, 46) -1.60229px 1.19694px 0px, rgb(204, 48, 46) -1.97998px 0.28224px 0px, rgb(204, 48, 46) -1.87291px -0.701566px 0px, rgb(204, 48, 46) -1.30729px -1.5136px 0px, rgb(204, 48, 46) -0.421592px -1.95506px 0px, rgb(204, 48, 46) 0.567324px -1.91785px 0px, rgb(204, 48, 46) 1.41734px -1.41108px 0px, rgb(204, 48, 46) 1.92034px -0.558831px 0px; position: relative; z-index: 1; } .mwstmm-subtitle { font-size: 12px; background: rgba(204, 48, 46, 0.4); padding: 3px 5px; border-radius: 5px; position: relative; z-index: 0; top: -8px; text-shadow: 0 1px 2px rgba(0,0,0,0.5); } .mwstmm-subtitle a:hover { text-decoration: underline; } .mwstmm-settings-option { background: var(--ds-color-purple-100); padding: 6px 10px; border-radius: 5px; font-size: 12px; cursor: pointer; opacity: 0.75; transition: opacity 0.2s; pointer-events: auto; } .mwstmm-settings-option:hover { opacity: 1; } #mwstmm-map-list h3 { margin-bottom: 10px; } #mwstmm-map-list .tag-input { display: block; width: 100%; font: inherit; border:1px solid #ccc; } #mwstmm-map-list .maps { max-height: 200px; overflow-x: hidden; overflow-y: auto; font-size: 15px; } #mwstmm-map-list .map { display: flex; justify-content: space-between; align-items: center; gap: 20px; padding: 8px; transition: background 0.2s; } #mwstmm-map-list .map:nth-child(2n) { background: #f0f0f0; } #mwstmm-map-list .map-buttons:not(.is-added) .map-added { display: none !important; } #mwstmm-map-list .map-buttons.is-added .map-add { display: none !important; } #mwstmm-map-list .map-add { background: green; color: #fff; padding: 3px 6px; border-radius: 5px; font-size: 13px; font-weight: bold; cursor: pointer; } #mwstmm-map-list .map-added { background: #000; color: #fff; padding: 3px 6px; border-radius: 5px; font-size: 13px; font-weight: bold; } div[class^="result-list_listItemWrapper__"] { position: relative; } div[class^="result-list_listItemWrapper__"] .mwstmm-settings-option { margin-left: auto; line-height: 1; align-self: center; } .swal-small-popup { position: absolute; width: auto !important; height: auto !important; top: -250px !important; font-weight: bold !important; font-size: 8px !important; text-align: center !important; display: flex !important; justify-content: center !important; align-items: center !important; } .tag-buttons { margin-top: 10px; } .tag-button { margin: 5px 5px 0 0; padding: 4px 10px; border: 1px solid #ccc; background: #f0f0f0; border-radius: 4px; cursor: pointer; transition: 0.2s; font-size: 16px; font-weight: bold; } .tag-button:hover { background: #e0e0e0; } .tag-button.active { background-color: green; color: white; border-color: green; } .swal2-input { font-size: 14px !important; } .swal2-select { font-size: 14px !important; } `); let detectButton, downloadButton; let previousListener, zoomLevel, w, h; let formattedTime, capturePano, type=10; let isHidden, cleanStyle; let LOCATION; let MAP_LIST; let previousMapId=JSON.parse(GM_getValue('previousMapId', null)); let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun','Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; let full_months=['January', 'February', 'March', 'April', 'May', 'June','July', 'August', 'September', 'October', 'November', 'December'] let dateSvg=` ` let date_Svg=` ` let iconSvg=` ` let icon_Svg=` ` let saveSvg=` ` let noteSvg=` ` let settingSvg=`` const iconUrl=svgToUrl(iconSvg) const icon_Url=svgToUrl(icon_Svg) const svgUrl=svgToUrl(dateSvg) const svg_Url=svgToUrl(date_Svg) const saveUrl=svgToUrl(saveSvg) const noteUrl=svgToUrl(noteSvg) const settingUrl=svgToUrl(settingSvg) const moon_phase=['🌑','🌒','🌓','🌔','🌕','🌖','🌗','🌘'] const mountain = "⛰️"; const wave = "🌊"; function svgToUrl(svgText) { const svgBlob = new Blob([svgText], {type: 'image/svg+xml'}); const svgUrl = URL.createObjectURL(svgBlob); return svgUrl; } async function showSettingsPopup(){ await Swal.fire({ title: '⚙️ Config Settings', html: `
`, confirmButtonText: '💾 Save Settings', denyButtonText: '🗑️ Clear Config', showDenyButton: true, focusConfirm: false, allowOutsideClick: true, allowEscapeKey: true, didOpen: () => { document.getElementById('swal-date').value = CONFIG.DATE_FORMAT; document.getElementById('swal-time').value = CONFIG.TIME_FORMAT; document.getElementById('swal-accuracy').value = CONFIG.ACCURACY; document.getElementById('swal-units').value = CONFIG.LENGTH_UNITS; document.getElementById('swal-api').value = CONFIG.MAP_MAKING_API_KEY; document.getElementById('swal-recent').value = CONFIG.NUMBER_OF_RECENT_MAPS; document.getElementById('swal-fullcheck').value = CONFIG.FULL_CHECK?.toString(); document.getElementById('swal-nearbycheck').value = String(CONFIG.NEARBY_CHECK); document.getElementById('swal-speedshow').value = CONFIG.SPEED_SHOW?.toString(); }, preConfirm: () => { const accuracy = parseInt(document.getElementById('swal-accuracy').value); const recentMaps = parseInt(document.getElementById('swal-recent').value); if (isNaN(accuracy) || accuracy <= 0 || !Number.isInteger(accuracy)) { Swal.showValidationMessage("Accuracy must be a positive integer."); return false; } if (isNaN(recentMaps) || recentMaps <= 0 || recentMaps > 10 || !Number.isInteger(recentMaps)) { Swal.showValidationMessage("Recent Maps to Show must be an integer between 1 and 10."); return false; } return { DATE_FORMAT: parseInt(document.getElementById('swal-date').value), TIME_FORMAT: parseInt(document.getElementById('swal-time').value), ACCURACY: accuracy, LENGTH_UNITS: document.getElementById('swal-units').value, MAP_MAKING_API_KEY: document.getElementById('swal-api').value.trim(), NUMBER_OF_RECENT_MAPS: recentMaps, FULL_CHECK: document.getElementById('swal-fullcheck').value === 'true', NEARBY_CHECK: Number(document.getElementById('swal-nearbycheck').value), SPEED_SHOW: document.getElementById('swal-speedshow').value === 'true' }; } }).then(async (result) => { if (result.isConfirmed && result.value) { localStorage.setItem("PANO_DETECTIVE_CONFIG", JSON.stringify(result.value)); CONFIG=result.value await Swal.fire({ title: ' ✔️ Settings saved successfully.', timer: 1500, showConfirmButton: false, allowOutsideClick:true, backdrop:null, customClass: { popup: "swal-small-popup" } }); } else if (result.isDenied) { localStorage.removeItem("PANO_DETECTIVE_CONFIG"); CONFIG=DEFAULT await Swal.fire({ title: '🗑️ Configuration cleared.', timer: 1500, showConfirmButton: false, allowOutsideClick:true, backdrop:null, customClass: { popup: "swal-small-popup" } }); } }); } function defaultState() { return { recentMaps: [] } } function loadState() { const data = GM_getValue('mwstmm_state', null) if(!data) return; const dataJson = JSON.parse(data); if(!data) return; Object.assign(MWSTMM_STATE, defaultState(), dataJson); saveState(); } function saveState() { GM_setValue('mwstmm_state', JSON.stringify(MWSTMM_STATE)); } const MWSTMM_STATE = defaultState(); loadState(); async function mmaFetch(url, options = {}) { const response = await fetch(new URL(url, 'https://map-making.app'), { ...options, headers: { accept: 'application/json', authorization: `API ${CONFIG.MAP_MAKING_API_KEY.trim()}`, ...options.headers } }); if (!response.ok) { let message = 'Unknown error'; try { const res = await response.json(); if (res.message) { message = res.message; } } catch { //empty } alert(`An error occurred while trying to connect to Map Making App. ${message}`); throw Object.assign(new Error(message), { response }); } return response; } async function getMaps() { const response = await mmaFetch(`/api/maps`); const maps = await response.json(); return maps; } async function importLocations(mapId, locations) { const response = await mmaFetch(`/api/maps/${mapId}/locations`, { method: 'post', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ edits: [{ action: { type: 4 }, create: locations, remove: [] }] }) }); await response.json(); } function extractParams(link) { const regex = /@(-?\d+\.\d+),(-?\d+\.\d+),.*?\/data=!3m\d+!1e\d+!3m\d+!1s([^!]+)!/; const match = link.match(regex); if (match && match.length === 4) { var lat = match[1]; var lng = match[2]; var panoId = match[3]; return {lat,lng,panoId} } else { console.error('Invalid Google Street View link format'); return null; } } function showLoader() { if(document.getElementById('mwstmm-loader')) return; const element = document.createElement('div'); element.id = 'mwstmm-loader'; element.className = 'mwstmm-modal'; element.innerHTML = `
LOADING...
`; document.body.appendChild(element); } function hideLoader() { const element = document.getElementById('mwstmm-loader'); if(element) element.remove(); } async function clickedMapButton() { if(CONFIG.MAP_MAKING_API_KEY === 'PASTE_YOUR_KEY_HERE') { await Swal.fire({ icon: 'warning', title: 'API Key Required', html: `To save locations to Map Making App, you must first configure your API key.

Please click the ⚙️ icon and enter your API key in the popup.`, confirmButtonText: 'Got it!', }); return; } if(!MAP_LIST) { showLoader(); try { MAP_LIST = await getMaps(); }catch{ //empty } hideLoader(); } if(MAP_LIST) { showMapList() } } function showMapList() { if(document.getElementById('mwstmm-map-list')) return; const element = document.createElement('div'); element.id = 'mwstmm-map-list'; element.className = 'mwstmm-modal'; let recentMapsSection = ``; if(CONFIG.NUMBER_OF_RECENT_MAPS > 0 && MWSTMM_STATE.recentMaps.length > 0) { let recentMapsHTML = ''; for(const m of MWSTMM_STATE.recentMaps) { if(m.archivedAt) continue; recentMapsHTML += `
${m.name} ADD ADDED
`; } recentMapsSection = `

Recent Maps

${recentMapsHTML}

`; } let mapsHTML = ''; let tagButtonsHTML = ''; if(LOCATION){ for (const tag of LOCATION.tagFields) { tagButtonsHTML += ``; } } for(const m of MAP_LIST) { if(m.archivedAt) continue; mapsHTML += `
${m.name} ADD ADDED
`; } element.innerHTML = `

Tags (comma separated)

${tagButtonsHTML}


${recentMapsSection}

All Maps

${mapsHTML}
`; document.body.appendChild(element); element.querySelector('.dim').addEventListener('click', closeMapList); document.getElementById('mwstmm-map-tags').addEventListener('keyup', e => e.stopPropagation()); document.getElementById('mwstmm-map-tags').addEventListener('keydown', e => e.stopPropagation()); document.getElementById('mwstmm-map-tags').addEventListener('keypress', e => e.stopPropagation()); document.getElementById('mwstmm-map-tags').focus(); for(const map of element.querySelectorAll('.maps .map-add')) { map.addEventListener('click', addLocationToMap); } for (const btn of element.querySelectorAll('.tag-button')) { btn.addEventListener('click', function () { const tag = this.dataset.tag; const input = document.getElementById('mwstmm-map-tags'); let currentTags = input.value.split(',') .map(t => t.trim()) .filter(t => t.length > 0); if (this.classList.contains('active')) { currentTags = currentTags.filter(t => t !== tag); this.classList.remove('active'); } else { if (!currentTags.includes(tag)) { currentTags.push(tag); } this.classList.add('active'); } input.value = currentTags.join(', '); }); } } function closeMapList() { const element = document.getElementById('mwstmm-map-list'); if(element) element.remove(); } function addLocationToMap(e) { e.target.parentNode.classList.add('is-added'); const id = parseInt(e.target.dataset.id); previousMapId=id GM_setValue('previousMapId', JSON.stringify(previousMapId)); if(CONFIG.NUMBER_OF_RECENT_MAPS > 0) { MWSTMM_STATE.recentMaps = MWSTMM_STATE.recentMaps.filter(e => e.id !== id).slice(0, CONFIG.NUMBER_OF_RECENT_MAPS-1); for(const map of MAP_LIST) { if(map.id === id) { MWSTMM_STATE.recentMaps.unshift(map); break; } } } saveState(); importLocations(id, [{ id: -1, location: {lat: LOCATION.lat, lng: LOCATION.lng}, panoId: LOCATION.panoId ?? null, heading: LOCATION.heading ?? 90, pitch: LOCATION.pitch ?? 0, zoom: LOCATION.zoom === 0 ? null : LOCATION.zoom, tags: document.getElementById('mwstmm-map-tags').value.split(',').map(t => t.trim()).filter(t => t.length > 0), flags: LOCATION.panoId ? 1 : 0 }]); } function addSettingsButtonsToPage() { const container = document.getElementById('image-header'); if(!container || document.getElementById('mwstmm-main')) return; const element = document.createElement('div'); element.id = 'mwstmm-main'; element.style.backgroundImage=`url(${saveUrl})` element.setAttribute('data-text',"Save to MapMaking") element.innerHTML = `
`; container.appendChild(element); setTimeout(() => { if(document.querySelector('.TrU0dc.NUqjXc')){ element.style.right='0.85rem' element.style.top='4rem' }}, 100) createSettingsButtonSummaryEvents(); } function parseMeta(data) { const pathRegex = /@([^,]+),([^,]+),(\d+)a,([^y]+)y,([^h]+)h,([^t]+)t/; const urlObj = new URL(window.location.href); const path = urlObj.pathname; const pathMatch = path.match(pathRegex); const tags=[] var heading = pathMatch ? pathMatch[5] : null; var t = pathMatch ? pathMatch[6] : null; const panoId=data[1][0][1][1]; const lat = data[1][0][5][0][1][0][2]; const lng = data[1][0][5][0][1][0][3]; const year = data[1][0][6][7][0]; const month = data[1][0][6][7][1]; const worldsize = data[1][0][2][2][0]; const history =data[1][0][5][0][8]; const links= data[1][0][5][0][3][0] if(!heading) heading=data[1][0][5][0][1][2]; if(!t) t=90; const date = new Date(year, month - 1); const formattedDate = date.toLocaleString('default', { month: 'short', year: 'numeric' }); let region, locality, road, country, altitude; try { country = data[1][0][5][0][1][4]; if (['TW', 'HK', 'MO'].includes(country)) { country = 'CN'; } } catch (e) { country = null; } try { const address = data[1][0][3][2][1][0]; const parts = address.split(',') if(parts.length > 1){ region = parts[parts.length-1].trim(); locality=parts[0].trim() } else { region = address; } } catch (e) { try{ const address=data[1][0][3][2][0][0] const parts = address.split(',') if(parts.length > 1){ region = parts[parts.length-1].trim(); locality=parts[0].trim() } else region = address; } catch(e){ region=null; } } try { road = data[1][0][5][0][12][0][0][0][2][0]; } catch (e) { road = null; } try{ altitude=data[1][0][5][0][1][1][0] } catch(e){ altitude=null; } const generation = String(data[1][0][4]).includes('Google')?getGeneration(worldsize, country, lat, date):'ari'; let camera; if (generation=='Gen4'){ if(['IN','PR'].includes(country))camera='smallcam' else if (['NA', 'PA' , 'OM', 'QA', 'EC'].includes(country))camera='gen4trekker' } let isNewRoad= !history ? 'newroad' : false const tagFields = [formattedDate, `${year}-${month}`, year, months[month-1], full_months[month-1], country, region, locality, road, generation,camera, altitude?altitude.toFixed(2)+'m':null, isNewRoad].filter(Boolean); return { lat, lng, panoId, year, month, country, region, locality, road, generation, links, history, heading:parseFloat(heading), pitch:parseFloat(t)-90, zoom:0, tags, tagFields } } function getGeneration(worldsize, country, lat, date) { if (!worldsize) return 'Ari'; if (worldsize === 1664) return 'Gen1'; if (worldsize === 8192) return 'Gen4'; if (worldsize === 6656) { const dateStr = date.toISOString().slice(0, 7); const gen2Countries = new Set(['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', 'SJ']); const gen3Dates = { 'BD': '2021-04', 'EC': '2022-03', 'FI': '2020-09', 'IN': '2021-10', 'LK': '2021-02', 'KH': '2022-10', 'LB': '2021-05', 'NG': '2021-06', 'ST': '2024-02', 'US': '2019-01', 'VN':'2021-01', }; if (dateStr >= (gen3Dates[country] || '9999-99')) { if(country === 'US' && lat > 52)return 'BadCam' if(country!='US')return 'BadCam' } if (gen2Countries.has(country) && dateStr <= '2011-11') { return dateStr >= '2010-09' ? 'Gen2/3' : 'Gen2'; } return 'Gen3'; } } function haversine(lat1, lng1, lat2, lng2) { const R = 6371; const toRad = Math.PI / 180; const φ1 = lat1 * toRad; const φ2 = lat2 * toRad; const Δφ = (lat2 - lat1) * toRad; const Δλ = (lng2 - lng1) * toRad; const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } async function seekDrivingEnd(data,timeRange){ try{ const startPano=data[1][0][1][1] const startLat=data[1][0][5][0][1][0][2] const startLng=data[1][0][5][0][1][0][3] const step1=data[1][0][5][0][3][0][1][0][1] const metaData = await UE('GetMetadata', step1); if(metaData&&metaData.length>1){ const linkPano=metaData[1][0][5][0][3][0][1][0][1]===startPano?metaData[1][0][5][0][3][0][2]:metaData[1][0][5][0][3][0][1] const step2=linkPano[0][1] const metaData_ = await UE('GetMetadata', step2); if(metaData_){ const linkPano=metaData_[1][0][5][0][3][0][1][0][1]===step2?metaData_[1][0][5][0][3][0][2]:metaData_[1][0][5][0][3][0][1] const step3=linkPano[0][1] const lat_=linkPano[2][0][2] const lng_=linkPano[2][0][3] const captureTime = await binarySearch({"lat":lat_,"lng":lng_},timeRange.startDate,timeRange.endDate,step3,1,15); return {time:captureTime,lat:lat_,lng:lng_} } } } catch(e){ console.error('Failed to seek pano'+e) return null } } async function getLOCATION(){ const currentUrl = window.location.href; const result=extractParams(currentUrl); if(!result) return const metaData = await UE('GetMetadata', result.panoId); if(metaData) LOCATION = parseMeta(metaData) } function createSettingsButtonSummaryEvents() { document.getElementById('mwstmm-main').addEventListener('click', async () => { await getLOCATION() clickedMapButton(); }); } function addResultButton(location, item) { const btn = document.createElement('div'); btn.className = `mwstmm-settings-option`; btn.textContent = `SAVE`; btn.addEventListener('click', () => { LOCATION = location; clickedMapButton(); }); item.appendChild(btn); } async function UE(t, e, s, d,r) { try { const url = `https://maps.googleapis.com/$rpc/google.internal.maps.mapsjs.v1.MapsJsInternalService/${t}`; let payload = createPayload(t, e,s,d,r); const response = await fetch(url, { 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) { var payload; if (mode === 'GetMetadata') { const length=coorData.length const contentInfoElements = document.querySelectorAll('[role="contentinfo"]'); contentInfoElements.forEach(element => { const spans = element.querySelectorAll('span'); spans.forEach(span => { if (span.textContent.includes('Google')) type=2 }); }); if(String(coorData).substring(0,4)=='CIHM') type=10 payload = [["apiv3"],["en","US"],[[[type,coorData]]],[[1, 2, 3, 4, 8, 6]]]; } else if (mode === 'SingleImageSearch') { payload=[["apiv3"],[[null,null,parseFloat(coorData.lat),parseFloat(coorData.lng)],r],[[null,null,null,null,null,null,null,null,null,null,[s,d]],null,null,null,null,null,null,null,[2],null,[[[type,true,2]]]],[[2,6]]]} else { throw new Error("Invalid mode!"); } return JSON.stringify(payload); } async function binarySearch(c, start,end,panoId,accuracy, radius) { let capture let response if(!accuracy)accuracy=CONFIG.ACCURACY if(!radius)radius=30 while( (end - start > accuracy)) { let mid=Math.round((start + end)/2) ; response = await UE("SingleImageSearch", c, start,end,radius); if (response&&response.length>1){ end=mid /*if (response[1][1][1]==panoId){ end=mid } else{ start=end end+=(start-mid)}*/ } else { start=end end+=(start-mid)} capture=Math.round((start + end)/2) } return capture } async function downloadPanoramaImage(panoId, fileName,w,h) { return new Promise(async (resolve, reject) => { try { const imageUrl= `https://streetviewpixels-pa.googleapis.com/v1/tile?cb_client=apiv3&panoid=${panoId}&output=tile&zoom=${zoomLevel}&nbt=0&fover=2`; const tileWidth = 512; const tileHeight = 512; const zoomTiles=[2,4,8,16,32] let tilesPerRow,tilesPerColumn if(type==2){ tilesPerRow = Math.min(Math.ceil(w / tileWidth),zoomTiles[zoomLevel-1]); tilesPerColumn = Math.min(Math.ceil(h / tileHeight),zoomTiles[zoomLevel-1]/2);} else{ tilesPerRow=Math.ceil(w / tileWidth) tilesPerColumn = Math.ceil(h/ tileHeight); } const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width =tilesPerRow * tileWidth; canvas.height = tilesPerColumn * tileHeight; if (w === 13312) { const sizeMap = { 4: [6656, 3328], 3: [3328, 1664], 2: [1664, 832], 1: [832, 416] }; if (sizeMap[zoomLevel]) { [canvas.width, canvas.height] = sizeMap[zoomLevel]; } } const loadTile = (x, y) => { return new Promise(async (resolveTile) => { let tile; let tileUrl = `${imageUrl}&x=${x}&y=${y}`; if(type==10)tileUrl=`https://lh3.ggpht.com/jsapi2/a/b/c/x${x}-y${y}-z${zoomLevel}/${panoId}` try { tile = await loadImage(tileUrl); ctx.drawImage(tile, x * tileWidth, y * tileHeight, tileWidth, tileHeight); resolveTile(); } catch (error) { console.error(`Error loading tile at ${x},${y}:`, error); resolveTile(); } }); }; let tilePromises = []; for (let y = 0; y < tilesPerColumn; y++) { for (let x = 0; x < tilesPerRow; x++) { tilePromises.push(loadTile(x, y)); } } await Promise.all(tilePromises); canvas.toBlob(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); resolve(); }, 'image/jpeg'); } catch (error) { Swal.fire({ title: 'Error Downloading ❌', timer: 1500, showConfirmButton: false, allowOutsideClick:true, backdrop:null, customClass: { popup: "swal-small-popup" }}) reject(error); } }); } async function getElevation(lat, lng) { const url = `https://api.open-meteo.com/v1/elevation?latitude=${lat}&longitude=${lng}`; try { const response = await fetch(url); if (!response.ok) { console.error(`HTTP error! Status: ${response.status}`); return null } const data = await response.json(); const altitude = data.elevation; if(altitude) return altitude[0] else return null } catch (error) { console.error('Error fetching elevation data:', error); return null } } async function loadImage(url) { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'Anonymous'; img.onload = () => resolve(img); img.onerror = () => reject(new Error(`Failed to load image from ${url}`)); img.src = url; }); } function monthToTimestamp(m) { const [year, month] = m const startDate =Math.round( new Date(year, month-1,0).getTime()/1000); const endDate =Math.round(new Date(year, month, 2).getTime()/1000) return { startDate, endDate }; } async function getLocal(coord, timestamp) { const systemTimezoneOffset = -new Date().getTimezoneOffset() * 60; try { var offset_hours, offset const timezone=await GeoTZ.find(coord[0],coord[1]) try{ offset = await GeoTZ.toOffset(timezone);} catch(error){ offset = await GeoTZ.toOffset(timezone[0]);} if(offset){ offset_hours=parseInt(offset/60) } else if (offset===0) offset_hours=0 const offsetDiff = systemTimezoneOffset -offset_hours*3600; const convertedTimestamp = timestamp -offsetDiff; return convertedTimestamp; } catch (e) { 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 (e){ console.error('Failed to get timezone data'+e) } } } function getMoonPhaseIcon(dayOfMonth) { const cycleDays = 29.53; const phaseIndex = Math.floor((dayOfMonth % cycleDays) / (cycleDays / moon_phase.length)); return moon_phase[phaseIndex]; } function formatTimestamp(timestamp) { var date_text,time_text const date = new Date(timestamp * 1000); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); if (CONFIG.DATE_FORMAT===2) date_text=`${year}/${month}/${day}` else if (CONFIG.DATE_FORMAT===3) date_text=`${day}/${month}/${year}` else if (CONFIG.DATE_FORMAT===4) date_text=`${month}/${day}/${year}` else if (CONFIG.DATE_FORMAT===5) date_text=`${months[parseInt(month-1)]} ${parseInt(day)}, ${year}` else if (CONFIG.DATE_FORMAT===6) date_text=`${parseInt(day)} ${months[parseInt(month-1)]}, ${year}` else if (CONFIG.DATE_FORMAT===7) { const lunarDate=chineseLunar.solarToLunar(date) date_text = `${year}-${month}-${day} ${chineseLunar.format(lunarDate, 'MD')} ${getMoonPhaseIcon(lunarDate.day)}`;} else date_text=`${year}-${month}-${day}` ; if(CONFIG.TIME_FORMAT===1) time_text=`${hours}:${minutes}:${seconds}` else{ const period = parseInt(hours) >= 12 ? 'pm' : 'am'; var _hours = parseInt(hours) % 12 || 12; _hours = String(_hours).padStart(2, '0'); time_text= `${_hours}:${minutes}:${seconds} ${period}`; } if(!CONFIG.DATE_FORMAT)return date.toLocaleString().replace('T', ' '); else return `${date_text} ${time_text}`; } function formatLatLng(lat, lng) { const latDirection = lat >= 0 ? 'N' : 'S'; const latAbs = Math.abs(lat).toFixed(4); const latStr = `${latAbs}°${latDirection}`; const lngDirection = lng >= 0 ? 'E' : 'W'; const lngAbs = Math.abs(lng).toFixed(4); const lngStr = `${lngAbs}°${lngDirection}`; return `${latStr}, ${lngStr}`; } function getButtonPosition(symbol, spotlight) { if (!symbol || !spotlight) return null; const rectSymbol = symbol.getBoundingClientRect(); const rectSpotlight = spotlight.getBoundingClientRect(); return rectSpotlight.top ? { top: rectSymbol.top, left: rectSymbol.left } : null; } function createButton(id) { const button = document.createElement("button"); button.id = id; return button; } function setupButton(button, backgroundUrl, position) { Object.assign(button.style, { backgroundImage: `url(${backgroundUrl})`, backgroundSize: "cover", backgroundPosition: "center", display: "block", width: "24px", height: "24px", fontSize: "12px", borderRadius: "10px", cursor: "pointer", backgroundColor: "transparent", ...(position && { position: "fixed", top: `${position.top + 40}px`, left: `${position.left - 25}px` }), }); } function addButtons(symbol, position) { if (!detectButton) detectButton = createButton("detect-button"); if (!downloadButton) downloadButton = createButton("download-button"); if (symbol&&symbol.parentNode&&!symbol.parentNode.contains(detectButton)) symbol.parentNode.appendChild(detectButton); if (symbol&&symbol.parentNode&&!symbol.parentNode.contains(downloadButton)) symbol.parentNode.appendChild(downloadButton); addButtonHoverEffect(detectButton, svg_Url, svgUrl); addButtonHoverEffect(downloadButton, icon_Url, iconUrl); downloadButton.addEventListener("click",async function(){ const { value: zoom, dismiss: inputDismiss } = await Swal.fire({ title: 'Image Quality', html: '', icon: 'question', showCancelButton: true, showCloseButton: true, allowOutsideClick: false, backdrop:null, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: 'Yes', cancelButtonText: 'Cancel', preConfirm: () => { return document.getElementById('zoom-select').value; } }); if (zoom){ zoomLevel=parseInt(zoom) const currentUrl = window.location.href; var panoId=extractParams(currentUrl).panoId; var panoDate,lat,lng const metaData = await UE('GetMetadata', panoId); if (!metaData) { console.error('Failed to get metadata'); return; } try{ w=parseInt(metaData[1][0][2][2][1]) h=parseInt(metaData[1][0][2][2][0]) lat=metaData[1][0][5][0][1][0][2] lng=metaData[1][0][5][0][1][0][3] } catch (error){ console.error(error) return } try{ panoDate=metaData[1][0][6][7] } catch(e){ console.error(e) } if(w&&h){ const gpsTag=formatLatLng(lat,lng) var timeTag='' if(panoId===capturePano&&formattedTime) timeTag=formattedTime else{ if(panoDate) timeTag=`${panoDate[0]}-${panoDate[1]}`} const fileName = `${gpsTag}(${timeTag}).jpg`; const swal = Swal.fire({ title: 'Downloading...', timer: 1500, showConfirmButton: false, allowOutsideClick:true, backdrop:null, allowEscapeKey: false, didOpen: () => { Swal.showLoading(); }, customClass: { popup: "swal-small-popup" } }); await downloadPanoramaImage(panoId, fileName,w,h); swal.close() Swal.fire({ title: 'Download Completed ✔️', timer: 1500, showConfirmButton: false, allowOutsideClick:true, backdrop:null, customClass: { popup: "swal-small-popup" }}) } } }) detectButton.addEventListener("click",async function() { let dateSpan = document.querySelector('span.lchoPb'); if (!dateSpan){ dateSpan = document.querySelector('span.mqX5ad'); if(!dateSpan){ dateSpan = document.querySelector('div.mqX5ad'); if(!dateSpan) { dateSpan = document.querySelector('div.lchoPb'); } } } const logoSpan=document.querySelector('span.ilzTS') const logoDiv=document.querySelector('div.p4x6kc') if (dateSpan){ dateSpan.innerHTML='loading...' } const currentUrl = window.location.href; var altitude var panoId=extractParams(currentUrl).panoId; var lat=extractParams(currentUrl).lat var lng=extractParams(currentUrl).lng if (panoId.length>22) type=3 try { const metaData = await UE('GetMetadata', panoId); if (!metaData) { console.error('Failed to get metadata'); return; } var panoDate try { panoDate = metaData[1][0][6][7]; capturePano=metaData[1][0][1][1] altitude=metaData[1][0][5][0][1][1][0] } catch (error) { try{ panoDate = metaData[1][0][6][7]; capturePano=metaData[1][0][1][1]} catch(error){ dateSpan.textContent='unknown' console.error('Failed to parse metadata') return } } /*if (logoSpan){ //GM_setClipboard(`${altitude},"${panoId}"`,'text') }*/ const timeRange = monthToTimestamp(panoDate); if (!timeRange) { console.error('Failed to convert panoDate to timestamp'); return; } try { const [captureTime, drivingEnd] = await Promise.all([ binarySearch({"lat": lat, "lng": lng}, timeRange.startDate, timeRange.endDate, panoId), CONFIG.SHOW_SPEED?seekDrivingEnd(metaData, timeRange):null ]); if (!captureTime) { console.error('Failed to get capture time'); return; } const localTime=await getLocal([lat,lng],captureTime) if(!localTime){ console.error('Failed to get exact time'); } formattedTime=formatTimestamp(localTime) if(dateSpan){ dateSpan.textContent = formattedTime; } if(logoSpan){ if(drivingEnd){ const timeConsume=Math.abs(captureTime-drivingEnd.time) var distance = haversine(lat, lng, drivingEnd.lat, drivingEnd.lng) if(CONFIG.LENGTH_UNITS==='Imperial')distance=distance * 0.621371 if (timeConsume != 0) { const timeInHours = timeConsume / 3600; const avgSpeed=distance / timeInHours; const unit=CONFIG.LENGTH_UNITS==='Imperial'? 'mph':'km/h' if(avgSpeed) { logoSpan.textContent=!avgSpeed?`? ${unit}`:`${Math.round(avgSpeed*100)/100} ${unit}` logoDiv.style.backgroundImage=`url(https://cdn.discordapp.com/emojis/776219536936402984.webp?size=100)` } } else{ logoSpan.textContent='? km/h'} } if(!altitude) altitude=await getElevation(lat,lng) if (altitude != null) { let displayAltitude = altitude; let altUnit = 'm'; if (CONFIG.LENGTH_UNITS === 'Imperial') { displayAltitude = altitude * 3.28084; // convert meters to feet altUnit = 'ft'; } const altIcon = displayAltitude > 50 ? mountain : wave; if(CONFIG.SHOW_SPEED){ logoSpan.textContent += ` ${altIcon} ${Math.round(displayAltitude * 100) / 100}${altUnit}`;} else{ logoSpan.textContent = ` ${altIcon} ${Math.round(displayAltitude * 100) / 100}${altUnit}`;}; } else { logoSpan.textContent += ' unknown'; } } } catch (error) { console.error(error); } } catch (error) { console.error(error); } }) } function checkPosition() { const symbol = document.querySelector("[jsaction='titlecard.settings']"); const spotlight = document.querySelector("[jsaction='titlecard.spotlight']"); const position = getButtonPosition(symbol, spotlight); if(symbol){ if(!position){ detectButton.style=null downloadButton.style=null } else{ detectButton.style.marginTop = '1px'; if(position.left&&position.top) downloadButton.style.marginLeft = '26px'; else downloadButton.style.marginLeft='1px' } } if (!document.getElementById("detect-button") && !document.getElementById("download-button")) { addButtons(symbol, position); } setupButton(detectButton, svgUrl, position); setupButton(downloadButton, iconUrl, position); } function observePositionChanges() { const observer = new MutationObserver(() => checkPosition()); observer.observe(document.body, { childList: true, subtree: true }); } async function addCustomButton() { const symbol = document.querySelector("[jsaction='titlecard.settings']"); const spotlight = document.querySelector("[jsaction='titlecard.spotlight']"); const position = getButtonPosition(symbol, spotlight); if (!symbol || !spotlight) return; addButtons(symbol, position); observePositionChanges(); } function calculateFOV(zoom) { const pi = Math.PI; const argument = (3 / 4) * Math.pow(2, 1 - zoom); const radians = Math.atan(argument); const degrees = (360 / pi) * radians; return degrees; } function drawSegmentedLine(ctx, x1, y1, x2, y2, alpha) { ctx.setLineDash([10, 5]); const midX1 = x1 + (x2 - x1) * 0.49; const midY1 = y1 + (y2 - y1) * 0.49; const midX2 = x1 + (x2 - x1) * 0.51; const midY2 = y1 + (y2 - y1) * 0.51; ctx.lineWidth = 1.5; ctx.setLineDash([10, 5]); ctx.globalAlpha = alpha; ctx.strokeStyle = "black"; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(midX1, midY1); ctx.stroke(); ctx.setLineDash([]); ctx.globalAlpha = 1.0; ctx.strokeStyle = "red"; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(midX1, midY1); ctx.lineTo(midX2, midY2); ctx.stroke(); ctx.lineWidth = 1.5; ctx.setLineDash([10, 5]); ctx.globalAlpha = alpha; ctx.strokeStyle = "black"; ctx.beginPath(); ctx.moveTo(midX2, midY2); ctx.lineTo(x2, y2); ctx.stroke(); ctx.globalAlpha = 1.0; } async function getShortUrl(pageUrl) { const url = 'https://www.google.com/maps/rpc/shorturl'; const result=extractParams(pageUrl) if(!result)return const panoId=result.panoId const urlObj = new URL(pageUrl); const path = urlObj.pathname; const pathRegex = /@([^,]+),([^,]+),(\d+)a,([^y]+)y,([^h]+)h,([^t]+)t/; const pathMatch = path.match(pathRegex); const lat = pathMatch ? pathMatch[1] : null; const lng = pathMatch ? pathMatch[2] : null; const a=pathMatch ? pathMatch[3] : null; const y = pathMatch ? pathMatch[4] : null; const h = pathMatch ? pathMatch[5] : null; const t = pathMatch ? pathMatch[6] : null; const pb = `!1shttps://www.google.com/maps/@${lat},${lng},${a}a,${y}y,${h}h,${t}t/data=*213m5*211e1*213m3*211s${panoId}*212e0*216shttps%3A%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fpanoid%3D${panoId}%26cb_client%3Dmaps_sv.share%26w%3D900%26h%3D600%26yaw%3D${h}%26pitch%3D${t-90}%26thumbfov%3D100*217i16384*218i8192?coh=205410&entry=tts!2m1!7e81!6b1`; const params = new URLSearchParams({ authuser: '0', hl: 'en', gl: 'us', pb: pb }).toString(); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `${url}?${params}`, onload: function (response) { if (response.status >= 200 && response.status < 300) { try { const text = response.responseText; const match = text.match(/"([^"]+)"/); if (match && match[1]) { resolve(match[1]); } else { reject('No URL found.'); } } catch (error) { reject('Failed to parse response: ' + error); } } else { reject('Request failed with status: ' + response.status); } }, onerror: function (error) { reject('Request error: ' + error); } }); }); } function addButtonHoverEffect(button, hoverImageUrl, defaultImageUrl) { button.addEventListener("mouseenter", function(event) { button.style.backgroundImage = `url(${hoverImageUrl})`; }); button.addEventListener("mouseleave", function(event) { button.style.backgroundImage = `url(${defaultImageUrl})`; }); } async function getCountryName(code) { const response = await fetch(`https://restcountries.com/v3.1/alpha/${code}`); const data = await response.json(); return data[0]?.name?.common || "Unknown"; } async function getAddressFromOSM(lat, lng) { try { const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&accept-language=en`; const response = await fetch(url); if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); const data = await response.json(); return data.address; } catch (error) { console.error("Error fetching address from OSM:", error); return "Unknown"; } } let lastUpdateQueryKey = ''; let lastUpdateResult = null; async function checkUpdateTypes() { const { country, region, year, month } = LOCATION; const queryKey = `${country}_${region}_${year}_${month}`; if (queryKey === lastUpdateQueryKey && lastUpdateResult) { return lastUpdateResult; } const params = new URLSearchParams({ country, region, year, month }).toString(); const url = `https://vs-update-map.netlify.app/.netlify/functions/checkUpdateType?${params}`; const result = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url, responseType: "json", onload: res => { try { const data = typeof res.response === 'object' ? res.response : JSON.parse(res.response); lastUpdateQueryKey = queryKey; lastUpdateResult = data; resolve(data); } catch (e) { reject(new Error("Failed to parse response JSON")); } }, onerror: err => reject(err) }); }); return result; } async function queryByLocation() { const { lat, lng, year, month } = LOCATION; let radius = 10000; if (CONFIG.NEARBY_CHECK) { radius = Number(CONFIG.NEARBY_CHECK) * 1000; } const queryKey = `${lat}_${lng}_${radius}_${year}_${month}`; if (queryKey === lastUpdateQueryKey && lastUpdateResult) { return lastUpdateResult; } const params = new URLSearchParams({ lat, lng, radius, year, month }).toString(); const url = `https://vs-update-map.netlify.app/.netlify/functions/queryByLocation?${params}`; const result = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url, responseType: "json", onload: res => { try { const data = typeof res.response === 'object' ? res.response : JSON.parse(res.response); lastUpdateQueryKey = queryKey; lastUpdateResult = data; resolve(data); } catch (e) { reject(new Error("Failed to parse response JSON")); } }, onerror: err => reject(err) }); }); return result; } async function generateUpdateReportText() { let genupdate; let countryName; if(!LOCATION)return try{ if (LOCATION.generation === 'ari'||!LOCATION.country||!LOCATION.region) { try { const address = await getAddressFromOSM(LOCATION.lat, LOCATION.lng); if (address) { LOCATION.country = address.country_code || LOCATION.country; LOCATION.countryName = address.country || LOCATION.countryName; LOCATION.region = LOCATION.region||address.state || address.region || address.province || address.county; LOCATION.locality = LOCATION.locality||address.city || address.city_district || address.town; LOCATION.road = address.road || LOCATION.road; countryName = LOCATION.countryName; } } catch (error) { console.error("Error fetching address:", error); } } if (LOCATION.history && LOCATION.links) { try { const dates = LOCATION.history.map(pano => [pano[1][0], pano[0]]).sort((a, b) => b[0] - a[0]); if (dates.length > 0) { const metaData = await UE('GetMetadata', LOCATION.links[dates[0][1]][0][1]); var generation = parseMeta(metaData).generation; //if (generation=='Gen2/3') generation ='Gen2update: / :Gen3' if(generation=='BadCam')generation='ari' genupdate = generation === 'Gen4' ? null : `:${generation.toLowerCase()}update:`; } } catch (error) { console.error("Error fetching metadata:", error); } } let pageUrl = window.location.href; const [currentLink, typeupdate, nearbyUpdate,fetchedCountryName] = await Promise.all([ getShortUrl(pageUrl), CONFIG.FULL_CHECK?checkUpdateTypes():null, CONFIG.NEARBY_CHECK?queryByLocation():null, LOCATION.generation !== 'ari' ? getCountryName(LOCATION.country || 'Unknown') : Promise.resolve(countryName) ]); let position_word if(LOCATION.locality){ if(LOCATION.road){ if(LOCATION.road==LOCATION.locality) position_word='on' else position_word='in' } else position_word='in' } else{ if(LOCATION.road){ position_word='on' } else position_word='in' } const activeTypes = [ typeupdate?.newyear && ':newyear:', typeupdate?.newcountry && ':newcountry:', typeupdate?.newregion && ':newregion:' ].filter(Boolean) const emojiMap = { ':newyear:': 'https://cdn.discordapp.com/emojis/1270086438054002699.webp', ':newcountry:': 'https://cdn.discordapp.com/emojis/972878615408676946.webp', ':newregion:': 'https://cdn.discordapp.com/emojis/972878598073634816.webp' }; const reportParts = [ `:flag_${LOCATION.country ? LOCATION.country.toLowerCase() : ''}:`, ...activeTypes, !LOCATION.history ? ':newroad:' : '', genupdate || '', (LOCATION.generation=='Gen4' && (['IN','PR'].includes(LOCATION.country)))? ':SmallCam:': '', `${full_months[LOCATION.month - 1]} ${LOCATION.year}`, `${position_word} ${LOCATION.locality || LOCATION.road || ''}${(LOCATION.locality || LOCATION.road) ? ', ' : ''}${LOCATION.region}, ${fetchedCountryName}`, currentLink || `https://www.google.com/maps/@?api=1&map_action=pano&pano=${LOCATION.panoId}` ].filter(Boolean); const reportText = reportParts.join(' '); GM_setClipboard(reportText, 'text'); const emojiHtml = activeTypes.map(type => `` ).join(''); Swal.fire({ title: `Copy Succeeded${nearbyUpdate&&nearbyUpdate.length>0?' (nearby update report exists) ':' '}✔️`, html: activeTypes.length>0?`
${emojiHtml}
`:null, timer: 1500, showConfirmButton: false, allowOutsideClick:true, backdrop:null, customClass: { popup: "swal-small-popup" } }); } catch(e){ console.error('Error generating update report text: '+e) Swal.fire({ title: 'Error Generating ❌', timer: 1500, showConfirmButton: false, allowOutsideClick:true, backdrop:null, customClass: { popup: "swal-small-popup" }}) } } function addNoteButtonToPage() { const container = document.getElementById('image-header'); if(!container || document.getElementById('note-btn')) return; const element = document.createElement('div'); element.id = 'note-btn'; element.style.backgroundImage=`url(${noteUrl})` element.setAttribute('data-text',"Generate update report") container.appendChild(element); element.addEventListener('click', async () => { await getLOCATION() await generateUpdateReportText() }); setTimeout(() => { if(document.querySelector('.TrU0dc.NUqjXc')){ element.style.right='4rem' element.style.top='4rem' }}, 100) } function adSettingsButtonToPage() { const container = document.getElementById('image-header'); if(!container || document.getElementById('settings-btn')) return; const element = document.createElement('div'); element.id = 'settings-btn'; element.style.backgroundImage=`url(${settingUrl})` element.setAttribute('data-text',"Settings") container.appendChild(element); element.addEventListener('click', async () => { await showSettingsPopup() }); setTimeout(() => { if(document.querySelector('.TrU0dc.NUqjXc')){ element.style.right='7.2rem' element.style.top='4rem' }}, 100) } function toggleElementHidden() { if(!isHidden){ cleanStyle = GM_addStyle(` #omnibox-container {display:none !important} #image-header {display:none !important} .widget-image-header-close, .widget-image-header-scrim, .watermark, .app-viewcard-strip, .scene-footer, .content-container, #titlecard {display:none !important}, #pane {display: none !important} `); isHidden = true; } else{ cleanStyle.remove() isHidden=false; } } let pageLoaded = false; let onFocus; const focus_canvas = document.createElement("canvas"); focus_canvas.style.zIndex=0 focus_canvas.style.position = "fixed"; focus_canvas.style.top = "0"; focus_canvas.style.left = "0"; focus_canvas.style.width = "100vw"; focus_canvas.style.height = "100vh"; focus_canvas.style.pointerEvents = "none"; focus_canvas.width = window.innerWidth; focus_canvas.height = window.innerHeight; const ctx = focus_canvas.getContext("2d"); drawSegmentedLine(ctx, 0, 0, focus_canvas.width, focus_canvas.height,0.8); drawSegmentedLine(ctx, focus_canvas.width, 0, 0, focus_canvas.height,0.8); let onKeyDown = async (e) => { if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA' || event.target.isContentEditable) { return; } if ((e.metaKey || e.shiftKey)&&(e.key === 'z' || e.key === 'Z')) { if(!previousMapId) return e.stopImmediatePropagation(); await getLOCATION() importLocations(previousMapId, [{ id: -1, location: {lat: LOCATION.lat, lng: LOCATION.lng}, panoId: LOCATION.panoId ?? null, heading: LOCATION.heading ?? 90, pitch: LOCATION.pitch ?? 0, zoom: LOCATION.zoom === 0 ? null : LOCATION.zoom, tags: LOCATION.tags, flags: LOCATION.panoId ? 1 : 0 }]); Swal.fire({ title: 'Import Succeeded ✔️', timer: 1500, showConfirmButton: false, allowOutsideClick:true, backdrop:null, customClass: { popup: "swal-small-popup" } }); } if (!e.ctrlKey&&!e.metaKey&&!e.shiftKey&&(e.key === 'x' || e.key === 'X')){ if (!onFocus){ onFocus=true document.body.appendChild(focus_canvas) } else{ onFocus=false document.body.removeChild(focus_canvas) } } if (!e.ctrlKey&&!e.metaKey&&!e.shiftKey&&(e.key === 'c' || e.key === 'c')) { e.stopImmediatePropagation(); let pageUrl = window.location.href; const currentLink=await getShortUrl(pageUrl) if(!currentLink) return GM_setClipboard(currentLink, 'text'); Swal.fire({ title: 'Copy Succeeded ✔️', timer: 1500, showConfirmButton: false, allowOutsideClick:true, backdrop:null, customClass: { popup: "swal-small-popup" } }); } if (!e.ctrlKey&&!e.metaKey&&(e.key === 'v' || e.key === 'V')) { e.stopImmediatePropagation(); await getLOCATION() await generateUpdateReportText() } if (!e.ctrlKey&&!e.metaKey &&(e.key === 'h' || e.key === 'H')) { e.stopImmediatePropagation(); toggleElementHidden() } } function onPageLoad() { if (pageLoaded) return; pageLoaded = true; const sceneFooter = document.getElementsByClassName('scene-footer')[0]; if (!sceneFooter) return; document.addEventListener("keydown", onKeyDown,true); const observer = new MutationObserver(function (mutationsList) { const navigationDiv = document.querySelector("[role='navigation']"); if (navigationDiv) { addCustomButton(); addSettingsButtonsToPage(); addNoteButtonToPage(); adSettingsButtonToPage(); } else { const element = document.getElementById('mwstmm-main'); if (element) element.remove(); const btn1 = document.getElementById('note-btn'); if (btn1) btn1.remove(); const btn2 = document.getElementById('settings-btn'); if (btn2) btn2.remove(); } }); const config = { childList: true, subtree: true, attributes: true }; observer.observe(sceneFooter, config); } if (!pageLoaded) { window.addEventListener('load', onPageLoad); } })();