// ==UserScript== // @name Pano Detective // @namespace https://greasyfork.org/users/1179204 // @version 1.9.0 // @description Find the exact time a Google Street View image was taken (default coverage) // @author KaKa // @match *://maps.google.com/* // @match *://www.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.pl/maps/* // @match *://*.google.se/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 // @license BSD // @downloadURL none // ==/UserScript== (function() { const DATE_FORMAT=0 // 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 const DISTANCE_UNITS='METERS' // or MILES /* ----- 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: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; } `); let detectButton,downloadButton,previousListener,zoomLevel,w,h,formattedTime,capturePano,type=10 let LOCATION, ROUNDS; 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 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=['🌑','🌒','🌓','🌔','🌕','🌖','🌗','🌘'] const mountain = "⛰️"; const wave = "🌊"; 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 = 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 ${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); previousMapId=id GM_setValue('previousMapId', JSON.stringify(previousMapId)); 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 ?? 90, pitch: LOCATION.pitch ?? 0, zoom: LOCATION.zoom === 0 ? null : LOCATION.zoom, tags: LOCATION.tags, 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 tilesize = data[1][0][2][2][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' }); 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 { const address = data[1][0][3][2][1][0]; const parts = address.split(',') if(parts.length > 1){ region = parts[parts.length-1].trim(); } else { region = address; } if(region)tags.push(region) } 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(); } else { region = address; } if(region)tags.push(region)} catch(e){} } 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', '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': 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'; } } } 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.log('Failed to seek pano'+e) return null } } 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 const contentInfoElements = document.querySelectorAll('[role="contentinfo"]'); contentInfoElements.forEach(element => { const spans = element.querySelectorAll('span'); spans.forEach(span => { if (span.textContent.includes('Google')) 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,accuracy, radius) { let capture let response if(!accuracy)accuracy=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('Error!', error.toString(),'error'); 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.log('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 (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-1)]} ${parseInt(day)}, ${year}` else if (DATE_FORMAT===6) date_text=`${parseInt(day)} ${months[parseInt(month-1)]}, ${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; if (!detectButton||!downloadButton){ 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]) lat=metaData[1][0][5][0][1][0][2] lng=metaData[1][0][5][0][1][0][3] } catch (error){ console.log(error) return } try{ panoDate=metaData[1][0][6][7] } catch(e){ console.log(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', 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() { 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), seekDrivingEnd(metaData, timeRange) ]); 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; } if(logoSpan && drivingEnd){ const timeConsume=Math.abs(captureTime-drivingEnd.time) var distance = haversine(lat, lng, drivingEnd.lat, drivingEnd.lng) if(DISTANCE_UNITS==='MILES')distance=distance * 0.621371 if (timeConsume != 0) { const timeInHours = timeConsume / 3600; const avgSpeed=distance / timeInHours; if(avgSpeed) { logoSpan.textContent=!avgSpeed?'? km/h':`${Math.round(avgSpeed*100)/100} km/h ` 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) logoSpan.textContent+=altitude ==null?'unknown':` ${altitude>50?mountain:wave} ${Math.round(altitude*100)/100}m` } } catch (error) { console.log(error); } } catch (error) { console.error(error); } }) buttonContainer.appendChild(downloadButton) buttonContainer.appendChild(detectButton) } else{ if(document.getElementById('detect-button')&&document.getElementById('download-button')) return else { const symbol=document.querySelector("[jsaction='titlecard.settings']") const buttonContainer = symbol.parentNode; buttonContainer.appendChild(downloadButton) buttonContainer.appendChild(detectButton) } } } 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)}, 100) } } 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})`; }); } let pageLoaded = false; function onPageLoad() { if (pageLoaded) return; pageLoaded = true; const sceneFooter = document.getElementsByClassName('scene-footer')[0]; if (!sceneFooter) { console.error('scene-footer element not found'); return; } const observer = new MutationObserver(function (mutationsList) { const navigationDiv = document.querySelector("[role='navigation']"); if (navigationDiv) { addCustomButton(); addSettingsButtonsToPage(); } else { const element = document.getElementById('mwstmm-main'); if (element) element.remove(); } }); const config = { childList: true, subtree: true, attributes: true }; observer.observe(sceneFooter, config); } if (!pageLoaded) { window.addEventListener('load', onPageLoad); } let onKeyDown = async (e) => { if (e.target.tagName === 'TEXTAREA') { return; } if ((e.ctrlKey || e.metaKey)&&(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 Succeed', text: 'Locations has been added into your map!', icon: 'success', timer: 1000, showConfirmButton: false, }); } 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); })();