// ==UserScript== // @name Pano Detective // @namespace https://greasyfork.org/users/1179204 // @version 1.6.4 // @description Find the exact time a Google Street View image was taken (default coverage) // @author KaKa // @match *://*.google.com/* // @match *://*.google.ru/* // @match *://*.google.de/* // @match *://*.google.fr/* // @match *://*.google.ca/* // @match *://*.google.it/* // @match *://*.google.pl/* // @match *://*.google.se/* // @match *://*.google.co.uk/* // @match *://*.google.co.jp/* // @match *://*.google.co.id/* // @match *://*.google.co.in/* // @match *://*.google.co.kr/* // @match *://*.google.co.za/* // @match *://*.google.com.hk/* // @match *://*.google.com.br/* // @match *://*.google.com.mx/* // @match *://*.google.com.ar/* // @match *://*.google.com.co/* // @match *://*.google.com.tw/* // @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 // @license BSD // @downloadURL none // ==/UserScript== (function() { const DATE_FORMAT=1 // 0:default(locale format) 1:yyyy-mm-dd 2:yyyy/mm/dd 3:dd/mm/yyyy 4:mm/dd/yyyy 5:Month dd, yyyy 6:dd Month, yyyy 7:Lunar const TIME_FORMAT=1 // 1:(hh:mm:ss) 2:(hh:mm:ss am/pm) const ACCURACY=2; // default setting is 2 seconds /* ----- API KEY INSTRUCTIONS ----- Requires an API key from Map Making App in order to save locations. Create one here: https://map-making.app/keys Make sure not to share this key with anybody or show it publicly as it will allow anybody to edit your maps. Replace `PASTE_YOUR_KEY_HERE` with your generated API key (make sure not to delete the quotes surrounding the key) */ const MAP_MAKING_API_KEY = "PASTE_YOUR_KEY_HERE"; // Number of maps to show in the "Recent Maps" section const NUMBER_OF_RECENT_MAPS = 3; /*=================================================================================================================================================================================================================================== =================================================================================================================================================================================================================================== =================================================================================================================================================================================================================================== ===================================================================================================================================================================================================================================*/ 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; } #mwstmm-main { position: absolute; width:36px; height:36px; top: 0.9rem; 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; } #mwstmm-main::after{ content: attr(data-text); position:absolute; top:120%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.8); color: #fff; padding: 5px; border-radius: 5px; font-weight:normal; font-size: 13px; line-height: 1; height: auto; white-space: nowrap; opacity: 0; transition: opacity 0.5s ease; } #mwstmm-main:hover::after { opacity: 1; } .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; } `); let detectButton,downloadButton,previousListener,zoomLevel,w,h,formattedTime,capturePano,type let LOCATION, ROUNDS; let MAP_LIST; let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun','Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; let dateSvg=` ` let date_Svg=` ` let iconSvg=` ` let icon_Svg=` ` let saveSvg=` ` 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 moon_phase=['🌑','🌒','🌓','🌔','🌕','🌖','🌗','🌘'] function svgToUrl(svgText) { const svgBlob = new Blob([svgText], {type: 'image/svg+xml'}); const svgUrl = URL.createObjectURL(svgBlob); return svgUrl; } function defaultState() { return { recentMaps: [] } } function loadState() { const data = window.localStorage.getItem('mwstmm_state'); if(!data) return; const dataJson = JSON.parse(data); if(!data) return; Object.assign(MWSTMM_STATE, defaultState(), dataJson); saveState(); } function saveState() { window.localStorage.setItem('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 ${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(MAP_MAKING_API_KEY === 'PASTE_YOUR_KEY_HERE') { alert('An API Key is required in order to save locations to Map Making App. Please add your API key by editing the Userscript and following the instructions at the top of the script.'); 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(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 = ''; for(const m of MAP_LIST) { if(m.archivedAt) continue; mapsHTML += `
${m.name} ADD ADDED
`; } element.innerHTML = `

Tags (comma separated)



${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); } } 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); if(NUMBER_OF_RECENT_MAPS > 0) { MWSTMM_STATE.recentMaps = MWSTMM_STATE.recentMaps.filter(e => e.id !== id).slice(0, 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, pitch: LOCATION.pitch, zoom: LOCATION.zoom === 0 ? null : LOCATION.zoom, tags: LOCATION.tags, flags: LOCATION.panoId ? 1 : 0 }]); } function addSettingsButtonsToPage() { const container = document.getElementById('app-container') 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 Map Making") element.innerHTML = `
`; container.appendChild(element); setTimeout(() => { if(document.querySelector('.g88MCb.S9kvJb')){ element.style.right='11rem' }}, 200) 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=[] const heading = pathMatch ? pathMatch[5] : null; const 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 tilesize = data[1][0][2][2][0]; const date = new Date(year, month - 1); const formattedDate = date.toLocaleString('default', { month: 'short', year: 'numeric' }); tags.push(formattedDate) let region, road, country; try { country = data[1][0][5][0][1][4]; if (['TW', 'HK', 'MO'].includes(country)) { country = 'CN'; } if(country) tags.push(country) } catch (e) { country = null; } try { region = data[1][0][3][2][1][0]; if(region) tags.push(region) } catch (e) { region = null; } try { road = data[1][0][5][0][12][0][0][0][2][0]; if(road) tags.push(road) } catch (e) { road = null; } const generation = getGeneration(tilesize, country, lat, date); if(generation) tags.push(generation) return { lat, lng, panoId, heading:parseFloat(heading), pitch:parseFloat(t)-90, zoom:0, tags } } function getGeneration(tilesize, country, lat, date) { if (tilesize) { if (tilesize === 1664) { return 'Gen1'; } else if (tilesize === 6656) { const dateStr = date.toISOString().slice(0, 7); const 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']; 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': null, 'US': '2019-01' }; if (dateStr >= gen3Dates[country]) { if (country === 'US' && lat > 52) return 'Shitcam'; return 'Shitcam'; } if (gen2Countries.includes(country)) return 'Gen2/3'; return 'Gen3'; } else if (tilesize === 8192) { return 'Gen4'; } } } async function getLOCATION(){ const currentUrl = window.location.href; var panoId=extractParams(currentUrl).panoId; const metaData = await UE('GetMetadata', 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 if (length>22){ type=10 } else type=2 payload = [["apiv3",null,null,null,"US",null,null,null,null,null,[[0]]],["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) { let capture let response while( (end - start > ACCURACY)) { let mid=Math.round((start + end)/2) ; response = await UE("SingleImageSearch", c, start,end,30); if (response&&response.length>1){ if (response[1][1][1]==panoId){ start=mid } else{ start=mid+start-end end=start-mid+end } } else { start=mid+start-end end=start-mid+end } capture=Math.round((start + end)/2) } return capture } async function downloadPanoramaImage(panoId, fileName,panoramaWidth,panoramaHeight) { 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=1&fover=2`; const tileWidth = 512; const tileHeight = 512; const zoomTiles=[2,4,8,16,32] const tilesPerRow = Math.min(Math.ceil(panoramaWidth / tileWidth),zoomTiles[zoomLevel-1]); const tilesPerColumn = Math.min(Math.ceil(panoramaHeight / tileHeight),zoomTiles[zoomLevel-1]/2); const canvasWidth = tilesPerRow * tileWidth; const canvasHeight = tilesPerColumn * tileHeight; const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = canvasWidth; canvas.height = canvasHeight; for (let y = 0; y < tilesPerColumn; y++) { for (let x = 0; x < tilesPerRow; x++) { const tileUrl = `${imageUrl}&x=${x}&y=${y}`; const tile = await loadImage(tileUrl); ctx.drawImage(tile, x * tileWidth, y * tileHeight, tileWidth, tileHeight); } } 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(); window.URL.revokeObjectURL(url); resolve(); }, 'image/jpeg'); } catch (error) { Swal.fire('Error!', error.toString(),'error'); reject(error); } }); } 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 const timezone=await GeoTZ.find(coord[0],coord[1]) const offset = await GeoTZ.toOffset(timezone); 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 (error) { throw error; } } 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 (DATE_FORMAT===2) date_text=`${year}/${month}/${day}` else if (DATE_FORMAT===3) date_text=`${day}/${month}/${year}` else if (DATE_FORMAT===4) date_text=`${month}/${day}/${year}` else if (DATE_FORMAT===5) date_text=`${months[parseInt(month)]} ${parseInt(day)}, ${year}` else if (DATE_FORMAT===6) date_text=`${parseInt(day)} ${months[parseInt(month)]}, ${year}` else if (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(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(!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}`; } async function addCustomButton() { const navigationDiv = document.querySelector("[role='navigation']"); if (!navigationDiv) return; detectButton= document.getElementById('detect-button') downloadButton= document.getElementById('download-button') if (!detectButton||!downloadButton){ const logoSpan=navigationDiv.querySelector('span.ilzTS') const logoDiv=navigationDiv.querySelector('div.p4x6kc') detectButton = document.createElement("button"); downloadButton = document.createElement("button"); const symbol=document.querySelector("[jsaction='titlecard.settings']") const buttonContainer = symbol.parentNode; if(symbol){ const spotlight=document.querySelector("[jsaction='titlecard.spotlight']") const rect_=spotlight.getBoundingClientRect() if(rect_.top){ const rect = symbol.getBoundingClientRect(); var absoluteTop = rect.top var absoluteLeft = rect.left} } detectButton.id = 'detect-button'; detectButton.style.marginTop = '1px'; setupButton(detectButton, svgUrl,absoluteTop,absoluteLeft); downloadButton.id = 'download-button'; setupButton(downloadButton, iconUrl,absoluteTop,absoluteLeft); if(absoluteLeft&&absoluteTop) downloadButton.style.marginLeft = '26px'; else downloadButton.style.marginLeft='1px' 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, 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]) panoDate=metaData[1][0][6][7] lat=metaData[1][0][5][0][1][0][2] lng=metaData[1][0][5][0][1][0][3] } catch (error){ try{ w=parseInt(metaData[1][2][2][1]) h=parseInt(metaData[1][2][2][0]) panoDate=metaData[1][6][7] lat=metaData[1][5][0][1][0][2] lng=metaData[1][5][0][1][0][3] } catch (error){ console.log(error) return } } if(w&&h){ const gpsTag=formatLatLng(lat,lng) var timeTag if(panoId===capturePano&&formattedTime) timeTag=formattedTime else timeTag=`${panoDate[0]}-${panoDate[1]}` const fileName = `${gpsTag}(${timeTag}).jpg`; const swal = Swal.fire({ title: 'Downloading', text: 'Please wait...', allowOutsideClick: false, allowEscapeKey: false, showConfirmButton: false, didOpen: () => { Swal.showLoading(); } }); await downloadPanoramaImage(panoId, fileName,w,h); swal.close() Swal.fire('Success!','Download completed', 'success'); } } }) detectButton.addEventListener("click",async function() { var dateSpan = navigationDiv.querySelector('span.lchoPb'); if (!dateSpan){ dateSpan = navigationDiv.querySelector('span.mqX5ad'); if(!dateSpan){ dateSpan = navigationDiv.querySelector('div.mqX5ad'); if(!dateSpan) { dateSpan = navigationDiv.querySelector('div.lchoPb'); } } } if (dateSpan){ dateSpan.textContent='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][6][7]; capturePano=metaData[1][1][1] altitude=metaData[1][5][0][1][1][0] } catch (error) { console.log(error); return; } } if (logoSpan){ logoSpan.textContent=`${altitude}m` logoDiv.style.backgroundImage=`url(${altitude>50?'https://www.svgrepo.com/show/406668/mountain.svg':'https://www.svgrepo.com/show/414747/ocean-sea-splash.svg'})` //GM_setClipboard(`${altitude},"${panoId}"`,'text') } if (!panoDate) { dateSpan.textContent='unknown' console.error('Failed to get panoDate'); return; } const timeRange = monthToTimestamp(panoDate); if (!timeRange) { console.error('Failed to convert panoDate to timestamp'); return; } try { const captureTime = await binarySearch({"lat":lat,"lng":lng},timeRange.startDate,timeRange.endDate,panoId); if (!captureTime) { console.error('Failed to get capture time'); return; } const exactTime=await getLocal([lat,lng],captureTime) if(!exactTime){ console.error('Failed to get exact time'); } formattedTime=formatTimestamp(exactTime) if(dateSpan){ dateSpan.textContent = formattedTime; } } catch (error) { console.log(error); } } catch (error) { console.error(error); } }) buttonContainer.appendChild(downloadButton) buttonContainer.appendChild(detectButton) } else return } function checkPosition(button,backgroundUrl) { const symbol=document.querySelector("[jsaction='titlecard.settings']") if(!symbol) return const rect = symbol.getBoundingClientRect(); const spotlight=document.querySelector("[jsaction='titlecard.spotlight']") const rect_=spotlight.getBoundingClientRect() var absoluteTop = rect.top var absoluteLeft = rect.left if(rect_.top){ if(absoluteTop&&absoluteLeft){ button.style.position='fixed' button.style.top=`${absoluteTop+40}px` button.style.left=`${absoluteLeft-25}px`}} else{ button.style.position=null button.style.top=null button.style.left=null } } function setupButton(button, backgroundUrl,top,left) { button.style.backgroundImage = `url(${backgroundUrl})`; button.style.backgroundSize = 'cover'; button.style.backgroundPosition = 'center'; button.style.display = 'block'; button.style.width = '24px'; button.style.height = '24px'; button.style.fontSize = '12px'; button.style.borderRadius = '10px'; button.style.cursor = 'pointer'; button.style.backgroundColor = 'transparent'; if(top&&left){ button.style.position='fixed' button.style.top=`${top+40}px` button.style.left=`${left-25}px` setInterval(function (){checkPosition(button,backgroundUrl)}, 200) } } 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; } async function getShortUrl(pageUrl) { const url = 'https://www.google.com/maps/rpc/shorturl'; const panoId=extractParams(pageUrl).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})`; }); } function onPageLoad() { const sceneFooter=document.getElementsByClassName('scene-footer')[0] const observer = new MutationObserver(function(mutationsList) { const controlContainer=document.getElementById('app-container') const navigationDiv = document.querySelector("[role='navigation']"); if(controlContainer){ addCustomButton() } if(navigationDiv){ addSettingsButtonsToPage()} else{ const element = document.getElementById('mwstmm-main'); if(element) element.remove(); } }); const config = { childList: true, subtree: true, attributes: true }; observer.observe(sceneFooter, config); } window.addEventListener('load', onPageLoad); let onKeyDown = async (e) => { if (e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) { return; } if ((e.ctrlKey || e.metaKey)&&(e.key === 'x' || e.key === 'X')) { e.stopImmediatePropagation(); let pageUrl = window.location.href; const currentLink=await getShortUrl(pageUrl) GM_setClipboard(currentLink, 'text'); Swal.fire({ title: 'Copy Succeed', text: 'Short Link has been pasted to your clipboard!', icon: 'success', timer: 1000, showConfirmButton: false, }); } } document.addEventListener("keydown", onKeyDown); })();